已有项目改造——Spring boot集成flyway

背景目前项目是spring boot+mysql+maven,因为需要数据库版本管理,因而集成flyway目标集成flyway对于已存在的数据库结构不影响步骤1.在pom.xml中加入依赖<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>5.2.4</version></dependency>2.在项目中,创建放置sql的文件夹cd src/main/resourcesmkdir db/migration3.导出当前数据库的DDL和数据文件清空数据库导出数据库 mysqldump -uroot -p Mydb >Mydb.sql清空数据库drop database Mydb;create schema Mydb default character set utf8 collate utf8_general_ci;4.将导出文件放置到db/migration中mv Mydb.sql db/migration/V1__init.sql5.编译&&运行项目#编译打包mvn package#运行项目java -jar target/myproject-0.0.1-SNAPSHOT.jar 6.运行结果数据库和未集成前一致,且多一个schema_version表,该表是flyway进行版本管理的主表注意这个方法简单粗暴,用于比数据量不大的情况一些可以配置的flyway参数方法一:在application.properties 或者 application.yml 中配置# FLYWAY (FlywayProperties)spring.flyway.baseline-description=<< Flyway Baseline >> # Description to tag an existing schema with when applying a baseline.spring.flyway.baseline-on-migrate=false # Whether to automatically call baseline when migrating a non-empty schema.spring.flyway.baseline-version=1 # Version to tag an existing schema with when executing baseline.spring.flyway.check-location=true # Whether to check that migration scripts location exists.spring.flyway.clean-disabled=false # Whether to disable cleaning of the database.spring.flyway.clean-on-validation-error=false # Whether to automatically call clean when a validation error occurs.spring.flyway.connect-retries=0 # Maximum number of retries when attempting to connect to the database.spring.flyway.enabled=true # Whether to enable flyway.spring.flyway.encoding=UTF-8 # Encoding of SQL migrations.spring.flyway.group=false # Whether to group all pending migrations together in the same transaction when applying them.spring.flyway.ignore-future-migrations=true # Whether to ignore future migrations when reading the schema history table.spring.flyway.ignore-ignored-migrations=false # Whether to ignore ignored migrations when reading the schema history table.spring.flyway.ignore-missing-migrations=false # Whether to ignore missing migrations when reading the schema history table.spring.flyway.ignore-pending-migrations=false # Whether to ignore pending migrations when reading the schema history table.spring.flyway.init-sqls= # SQL statements to execute to initialize a connection immediately after obtaining it.spring.flyway.installed-by= # Username recorded in the schema history table as having applied the migration.spring.flyway.locations=classpath:db/migration # Locations of migrations scripts. Can contain the special “{vendor}” placeholder to use vendor-specific locations.spring.flyway.mixed=false # Whether to allow mixing transactional and non-transactional statements within the same migration.spring.flyway.out-of-order=false # Whether to allow migrations to be run out of order.spring.flyway.password= # Login password of the database to migrate.spring.flyway.placeholder-prefix=${ # Prefix of placeholders in migration scripts.spring.flyway.placeholder-replacement=true # Perform placeholder replacement in migration scripts.spring.flyway.placeholder-suffix=} # Suffix of placeholders in migration scripts.spring.flyway.placeholders= # Placeholders and their replacements to apply to sql migration scripts.spring.flyway.repeatable-sql-migration-prefix=R # File name prefix for repeatable SQL migrations.spring.flyway.schemas= # Scheme names managed by Flyway (case-sensitive).spring.flyway.skip-default-callbacks=false # Whether to skip default callbacks. If true, only custom callbacks are used.spring.flyway.skip-default-resolvers=false # Whether to skip default resolvers. If true, only custom resolvers are used.spring.flyway.sql-migration-prefix=V # File name prefix for SQL migrations.spring.flyway.sql-migration-separator=__ # File name separator for SQL migrations.spring.flyway.sql-migration-suffixes=.sql # File name suffix for SQL migrations.spring.flyway.table=flyway_schema_history # Name of the schema schema history table that will be used by Flyway.spring.flyway.target= # Target version up to which migrations should be considered.spring.flyway.url= # JDBC url of the database to migrate. If not set, the primary configured data source is used.spring.flyway.user= # Login user of the database to migrate.spring.flyway.validate-on-migrate=true # Whether to automatically call validate when performing a migration.方法二:用环境变量配置略参考资料Execute Flyway Database Migrations on Startupspring boot 开箱集成flywayExisting database setup ...

March 5, 2019 · 2 min · jiezi

SpringBoot 实战 (十六) | 整合 WebSocket 基于 STOMP 协议实现广播消息

前言如题,今天介绍的是 SpringBoot 整合 WebSocket 实现广播消息。什么是 WebSocket ?WebSocket 为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服务器发送信息,反之也成立。WebSocket 是通过一个 socket 来实现双工异步通信能力的,但直接使用 WebSocket ( 或者 SockJS:WebSocket 协议的模拟,增加了当前浏览器不支持使用 WebSocket 的兼容支持) 协议开发程序显得十分繁琐,所以使用它的子协议 STOMP。STOMP 协议简介它是高级的流文本定向消息协议,是一种为 MOM (Message Oriented Middleware,面向消息的中间件) 设计的简单文本协议。它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STOMP 消息代理 (Broker) 进行交互,类似于 OpenWire (一种二进制协议)。由于其设计简单,很容易开发客户端,因此在多种语言和多种平台上得到广泛应用。其中最流行的 STOMP 消息代理是 Apache ActiveMQ。STOMP 协议使用一个基于 (frame) 的格式来定义消息,与 Http 的 request 和 response 类似 。广播接下来,实现一个广播消息的 demo。即服务端有消息时,将消息发送给所有连接了当前 endpoint 的浏览器。准备工作SpringBoot 2.1.3IDEAJDK8Pom 依赖配置<dependencies> <!– thymeleaf 模板引擎 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!– web 启动类 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– WebSocket 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!– test 单元测试 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>代码注释很详细,不多说。配置 WebSocket实现 WebSocketMessageBrokerConfigurer 接口,注册一个 STOMP 节点,配置一个广播消息代理@Configuration// @EnableWebSocketMessageBroker注解用于开启使用STOMP协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)// 开始支持@MessageMapping,就像是使用@requestMapping一样。@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //注册一个 Stomp 的节点(endpoint),并指定使用 SockJS 协议。 registry.addEndpoint("/endpointNasus").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 广播式配置名为 /nasus 消息代理 , 这个消息代理必须和 controller 中的 @SendTo 配置的地址前缀一样或者全匹配 registry.enableSimpleBroker("/nasus"); }}消息类客户端发送给服务器:public class Client2ServerMessage { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }}服务器发送给客户端:public class Server2ClientMessage { private String responseMessage; public Server2ClientMessage(String responseMessage) { this.responseMessage = responseMessage; } public String getResponseMessage() { return responseMessage; } public void setResponseMessage(String responseMessage) { this.responseMessage = responseMessage; }}演示控制器代码@RestControllerpublic class WebSocketController { @MessageMapping("/hello") // @MessageMapping 和 @RequestMapping 功能类似,浏览器向服务器发起消息,映射到该地址。 @SendTo("/nasus/getResponse") // 如果服务器接受到了消息,就会对订阅了 @SendTo 括号中的地址的浏览器发送消息。 public Server2ClientMessage say(Client2ServerMessage message) throws Exception { Thread.sleep(3000); return new Server2ClientMessage(“Hello,” + message.getName() + “!”); }}引入 STOMP 脚本将 stomp.min.js (STOMP 客户端脚本) 和 sockJS.min.js (sockJS 客户端脚本) 以及 Jquery 放在 resource 文件夹的 static 目录下。演示页面<!DOCTYPE html><html xmlns:th=“http://www.thymeleaf.org”><head> <meta charset=“UTF-8” /> <title>Spring Boot+WebSocket+广播式</title></head><body onload=“disconnect()"><noscript><h2 style=“color: #ff0000”>貌似你的浏览器不支持websocket</h2></noscript><div> <div> <button id=“connect” onclick=“connect();">连接</button> <button id=“disconnect” disabled=“disabled” onclick=“disconnect();">断开连接</button> </div> <div id=“conversationDiv”> <label>输入你的名字</label><input type=“text” id=“name” /> <button id=“sendName” onclick=“sendName();">发送</button> <p id=“response”></p> </div></div><script th:src=”@{sockjs.min.js}"></script><script th:src=”@{stomp.min.js}"></script><script th:src=”@{jquery.js}"></script><script type=“text/javascript”> var stompClient = null; function setConnected(connected) { document.getElementById(‘connect’).disabled = connected; document.getElementById(‘disconnect’).disabled = !connected; document.getElementById(‘conversationDiv’).style.visibility = connected ? ‘visible’ : ‘hidden’; $(’#response’).html(); } function connect() { // 连接 SockJs 的 endpoint 名称为 “/endpointNasus” var socket = new SockJS(’/endpointNasus’); // 使用 STOMP 子协议的 WebSocket 客户端 stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { setConnected(true); console.log(‘Connected: ’ + frame); // 通过 stompClient.subscribe 订阅 /nasus/getResponse 目标发送的信息,对应控制器的 SendTo 定义 stompClient.subscribe(’/nasus/getResponse’, function(respnose){ // 展示返回的信息,只要订阅了 /nasus/getResponse 目标,都可以接收到服务端返回的信息 showResponse(JSON.parse(respnose.body).responseMessage); }); }); } function disconnect() { // 断开连接 if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log(“Disconnected”); } function sendName() { // 向服务端发送消息 var name = $(’#name’).val(); // 通过 stompClient.send 向 /hello (服务端)发送信息,对应控制器 @MessageMapping 中的定义 stompClient.send("/hello”, {}, JSON.stringify({ ’name’: name })); } function showResponse(message) { // 接收返回的消息 var response = $("#response"); response.html(message); }</script></body></html>页面 Controller注意,这里使用的是 @Controller 注解,用于匹配 html 前缀,加载页面。@Controllerpublic class ViewController { @GetMapping("/nasus") public String getView(){ return “nasus”; }}测试结果打开三个窗口访问 http://localhost:8080/nasus ,初始页面长这样:三个页面全部点连接,点击连接订阅 endpoint ,如下图:在第一个页面,输入名字,点发送 ,如下图:在第一个页面发送消息,等待 3 秒,结果是 3 个页面都接受到了服务端返回的信息,广播成功。源码下载:https://github.com/turoDog/De…如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 5, 2019 · 2 min · jiezi

SpringBoot + MyBatisPlus + ShardingJDBC 分库分表读写分离整合

本文描述在本地数据库模拟分库分表、读写分离的整合实现,假定会员数据按照 ID 取模进行分库分表,分为 2 个主库,每个库分配一个读库,累计 100 张表。如下表所示:库主/从表user_1主t_user_00 ~ t_user_49user_slave_1从t_user_00 ~ t_user_49user_2主t_user_50 ~ t_user_99user_slave_2从t_user_50 ~ t_user_99本文主要展示核心代码,部分如 Controller、Service 层的测试代码实现非常简单,故而省略这部分代码。依赖版本<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc</artifactId> <version>3.0.0.M1</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version></dependency><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.41</version></dependency>数据准备use user_1;CREATE TABLE t_user_00 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_01 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_02 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_2;CREATE TABLE t_user_50 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_51 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_52 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_slave_1;CREATE TABLE t_user_00 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_01 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_02 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_slave_2;CREATE TABLE t_user_50 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_51 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_52 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;代码实现数据源配置server: port: 23333spring: application: name: pt-framework-demo datasource: type: com.alibaba.druid.pool.DruidDataSourcedatasource: default: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user: master: user1: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user2: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true slave: user1: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_slave_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user2: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_slave_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true主从、读写分离/** * Created by Captain on 01/03/2019. /@Configuration@MapperScan(basePackages = {“com.xxxx.framework.usermapper”}, sqlSessionFactoryRef = “userShardingSqlSessionFactory”)public class UserShardingDBConfiguration { @Value("${spring.datasource.type}") private Class<? extends DataSource> dataSourceType; private static final String USER_1_MASTER = “dsUser1Master”; private static final String USER_1_SLAVE = “dsUser1Slave”; private static final String USER_2_MASTER = “dsUser2Master”; private static final String USER_2_SLAVE = “dsUser2Slave”; private static final String USER_SHARDING_1 = “dsMasterSlave1”; private static final String USER_SHARDING_2 = “dsMasterSlave2”; private static final String USER_SHARDING_DATA_SOURCE = “userSharding”; @Bean(USER_1_MASTER) @ConfigurationProperties(prefix = “datasource.user.master.user1”) public DataSource dsUser1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_2_MASTER) @ConfigurationProperties(prefix = “datasource.user.master.user2”) public DataSource dsUser2(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_1_SLAVE) @ConfigurationProperties(prefix = “datasource.user.slave.user1”) public DataSource dsUserSlave1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } /* * user_2 * @return / @Bean(USER_2_SLAVE) @ConfigurationProperties(prefix = “datasource.user.slave.user2”) public DataSource dsUserSlave2(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_SHARDING_1) public DataSource masterSlave1(@Qualifier(USER_1_MASTER) DataSource dsUser1,@Qualifier(USER_1_SLAVE) DataSource dsUserSlave1) throws Exception { Map<String,DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(USER_1_MASTER, dsUser1); dataSourceMap.put(USER_1_SLAVE, dsUserSlave1); MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration(“dsUser1”, USER_1_MASTER, Lists.newArrayList(USER_1_SLAVE)); return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>()); } @Bean(USER_SHARDING_2) public DataSource masterSlave2(@Qualifier(USER_2_MASTER) DataSource dsUser2,@Qualifier(USER_2_SLAVE) DataSource dsUserSlave2) throws Exception { Map<String,DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(USER_2_MASTER, dsUser2); dataSourceMap.put(USER_2_SLAVE, dsUserSlave2); MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration(“dsUser2”, USER_2_MASTER, Lists.newArrayList(USER_2_SLAVE)); return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>()); } @Bean(USER_SHARDING_DATA_SOURCE) public DataSource dsUser(@Qualifier(USER_SHARDING_1) DataSource dsUser1, @Qualifier(USER_SHARDING_2) DataSource dsUser2) throws Exception { Map<String, DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(“dsUser1”, dsUser1); dataSourceMap.put(“dsUser2”, dsUser2); ShardingRuleConfiguration userRule = getUserRule(); userRule.setDefaultDataSourceName(“dsUser”); return ShardingDataSourceFactory.createDataSource(dataSourceMap, userRule, new ConcurrentHashMap<>(), new Properties()); } /* * 配置分片规则 * @return / private ShardingRuleConfiguration getUserRule(){ ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”, new MemberIdShardingSchemeAlgorithm())); shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”,new MemberIdShardingTableAlgorithm())); shardingRuleConfig.getBindingTableGroups().add(“t_user”); return shardingRuleConfig; } @Bean(“userShardingSqlSessionFactory”) public SqlSessionFactory userSqlSessionFactory(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource) throws Exception{ MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:usermapper/.xml”)); return sqlSessionFactoryBean.getObject(); } @Bean(“userTransaction”) public DataSourceTransactionManager userTransactionManager(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource){ return new DataSourceTransactionManager(dataSource); }}分库策略/** * CoreUser 分库策略 * Created by Captain on 01/03/2019. /public class MemberIdShardingSchemeAlgorithm implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { for ( String str : availableTargetNames ){ int index = shardingValue.getValue() % 100; return str + (index > 49 ? “2” : “1”); } return null; }}分表策略/* * 会员信息分表策略,按照 id 分表 * Created by Captain on 04/03/2019. /public class MemberIdShardingTableAlgorithm implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { int index = shardingValue.getValue() % 100; return shardingValue.getLogicTableName() + “_” + (index < 10 ? “0” + index : index + “”); }}实体类/* * Created by Captain on 01/03/2019. /@TableName(“t_user”)public class User { @TableId(type = IdType.INPUT) private Integer id; private String name; private Integer age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; }}Mapper/* * Created by Captain on 04/03/2019. */public interface UserMapper extends BaseMapper<User> {}测试预期模拟过程没有实际做主从同步,写入“主库”中的数据并不能自动同步至“从库”,因此,插入数据后,需要手动写入数据至对应的从库,并且可对数据进行差异写入,测试查询时可根据差异来判断读写分离是否生效。测试用例预期结果插入数据 id 指定为 8902user_1 中数据写入成功插入数据 id 指定为 8952user_2 中数据写入成功查询 id 为 8902 的数据查询到 user_slave_1 中的结果查询 id 为 8952 的数据查询到 user_slave_2 中的结果 ...

March 4, 2019 · 5 min · jiezi

SpringBoot 实战 (十五) | 服务端参数校验之一

前言估计很多朋友都认为参数校验是客户端的职责,不关服务端的事。其实这是错误的,学过 Web 安全的都知道,客户端的验证只是第一道关卡。它的参数验证并不是安全的,一旦被有心人抓到可乘之机,他就可以有各种方法来摸拟系统的 Http 请求,访问数据库的关键数据。轻则导致服务器宕机,重则泄露数据。所以,这时就需要设置第二道关卡,服务端验证了。老项目的服务端校验@RestController@RequestMapping("/student")public class ValidateOneController { @GetMapping("/id") public Student findStudentById(Integer id){ if(id == null){ logger.error(“id 不能为空!"); throw new NullPointerException(“id 不能为空”); } return studentService.findStudentById(id); }}看以上代码,就一个的校验就如此麻烦。那我们是否有好的统一校验方法呢?鉴于 SpringBoot 无所不能。答案当然是有的。其中,Bean Validator 和 Hibernate Validator 就是两套用于验证的框架,二者都遵循 JSR-303 ,可以混着用,鉴于二者的某些 Validator 注解有差别,例如 @Length 在 Bean Validator 中是没有的,所以这里我选择混合用。JSR-303JSR-303 是JAVA EE 6 中的一项子规范,叫做 Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现, Hibernate Validator 提供了 JSR 303 规范中所有内置 Constraint(约束) 的实现,除此之外还有一些附加的 Constraint 。这些 Constraint (约束) 全都通过注解的方式实现,请看下面两个表。Bean Validation 中内置的约束:注解作用@Null被注解参数必须为空@NotNull被注解参数不能为空@AssertTrue被注解参数必须为 True@AssertFalse被注解参数必须为 False@Min(value)被注解参数必须是数字,且其值必须大于等于 value@Max(value)被注解参数必须是数字,且其值必须小于等于 value@DecimaMin(value)被注解参数必须是数字,且其值必须大于等于 value@DecimaMax(value)被注解参数必须是数字,且其值必须小于等于 value@Size(max, min)被注解参数大小必须在指定范围内@Past被注解参数必须是一个过去的日期@Future被注解参数必须是一个将来的日期@Pattern(value)被注解参数必须符合指定的正则表达式@Digits(integer, fraction)被注解参数必须是数字,且其值必须在可接受范围内@NotBlank被注解参数的值不为空(不为 null、去除首位空格后长度为 0),不同于 @NotEmpty,@NotBlank 只应用于字符串且在比较时会去除字符串的空格Hibernate Validator 附加的约束:注解作用@NotEmpty被注解参数的值不为 null 且不为空(字符串长度不为0、集合大小不为0)@Email被注解参数必须是电子邮箱地址@Length被注解的字符串长度必须在指定范围内@Range被注解的参数必须在指定范围内准备工作SpringBoot 2.1.3IDEAJDK8Pom 文件依赖<!– web 启动类 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><!– test 单元测试类 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency><!– lombok 依赖用于简化 bean –><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency>实体类用于测试,加入了参数校验规则。@Data@AllArgsConstructor@NoArgsConstructorpublic class Student { private Integer id; @NotBlank(message = “学生名字不能为空”) @Length(min = 2, max = 10, message = “name 长度必须在 {min} - {max} 之间”) private String name; @NotNull(message = “年龄不允许为空”) @Min(value = 0, message = “年龄不能低于 {value} 岁”) private Integer age;}Controller 层写了两个方法,一个用于校验普通参数,一个用于校验对象@Validated //开启数据校验,添加在类上用于校验方法,添加在方法参数中用于校验参数对象。(添加在方法上无效)@RestController@RequestMapping("/student”)public class ValidateOneController { /** * 普通参数校验 * @param name * @return / @GetMapping("/name") public String findStudentByName(@NotBlank(message = “学生名字不能为空”) @Length(min = 2, max = 10, message = “name 长度必须在 {min} - {max} 之间”)String name){ return “success”; } /* * 对象校验 * @param student * @return */ @PostMapping("/add") public String addStudent(@Validated @RequestBody Student student){ return “success”; }}Postman 测试校验普通参数测试结果:下图可以看见,我没有在 http://localhost:8080/student/name 地址后添加 name 参数,传到后台马上就校验出异常了。而这个异常信息就是我定义的校验异常信息。校验对象测试结果:结果有点长:下图可以看见,我访问 http://localhost:8080/student/add 传入了参数对象,但对象是不能通过校验规则的,比如 age 参数为负数,name 参数长度太大,传到后台马上就校验出异常了。而这个异常信息就是我定义的校验异常信息。完整代码https://github.com/turoDog/De…如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 4, 2019 · 2 min · jiezi

SpringBoot整合Scala构建Web服务

今天我们尝试Spring Boot整合Scala,并决定建立一个非常简单的Spring Boot微服务,使用Scala作为编程语言进行编码构建。创建项目初始化项目mvn archetype:generate -DgroupId=com.edurt.ssi -DartifactId=springboot-scala-integration -DarchetypeArtifactId=maven-archetype-quickstart -Dversion=1.0.0 -DinteractiveMode=false修改pom.xml增加java和scala的支持<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.edurt.ssi</groupId> <artifactId>springboot-scala-integration</artifactId> <packaging>jar</packaging> <version>1.0.0</version> <name>springboot-scala-integration</name> <description>SpringBoot Scala Integration is a open source springboot, scala integration example.</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.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> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <!– dependency config –> <dependency.scala.version>2.12.1</dependency.scala.version> <!– plugin config –> <plugin.maven.scala.version>3.1.3</plugin.maven.scala.version> </properties> <dependencies> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${dependency.scala.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <sourceDirectory>${project.basedir}/src/main/scala</sourceDirectory> <testSourceDirectory>${project.basedir}/src/test/scala</testSourceDirectory> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>${plugin.maven.scala.version}</version> <executions> <execution> <goals> <goal>compile</goal> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>一个简单的应用类package com.edurt.ssiimport org.springframework.boot.SpringApplicationimport org.springframework.boot.autoconfigure.SpringBootApplication@SpringBootApplicationclass SpringBootScalaIntegrationobject SpringBootScalaIntegration extends App{ SpringApplication.run(classOf[SpringBootScalaIntegration])}添加Rest API接口功能创建一个HelloController Rest API接口,我们只提供一个简单的get请求获取hello,scala输出信息package com.edurt.ssi.controllerimport org.springframework.web.bind.annotation.{GetMapping, RestController}@RestControllerclass HelloController { @GetMapping(value = Array(“hello”)) def hello(): String = { return “hello,scala” }}修改SpringBootScalaIntegration文件增加以下设置扫描路径@ComponentScan(value = Array( “com.edurt.ssi.controller”))添加页面功能修改pom.xml文件增加以下页面依赖<!– mustache –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mustache</artifactId></dependency>修改SpringBootScalaIntegration文件增加以下设置扫描路径ComponentScan的value字段中"com.edurt.ssi.view"在src/main/resources路径下创建templates文件夹在templates文件夹下创建一个名为hello.mustache的页面文件<h1>Hello, Scala</h1>创建页面转换器HelloViewpackage com.edurt.ssi.viewimport org.springframework.stereotype.Controllerimport org.springframework.web.bind.annotation.GetMapping@Controllerclass HelloView { @GetMapping(value = Array(“hello_view”)) def helloView: String = { return “hello”; }}浏览器访问http://localhost:8080/hello_view即可看到页面内容添加数据持久化功能修改pom.xml文件增加以下依赖(由于测试功能我们使用h2内存数据库)<!– data jpa and db –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope></dependency>修改SpringBootScalaIntegration文件增加以下设置扫描model路径@EntityScan(value = Array( “com.edurt.ssi.model”))创建User实体package com.edurt.ssi.modelimport javax.persistence.{Entity, GeneratedValue, Id}@Entityclass UserModel { @Id @GeneratedValue var id: Long = 0 var name: String = null}创建UserSupport dao数据库操作工具类package com.edurt.ssi.supportimport com.edurt.ssi.model.UserModelimport org.springframework.data.repository.PagingAndSortingRepositorytrait UserSupport extends PagingAndSortingRepository[UserModel, Long] {}创建UserService服务类package com.edurt.ssi.serviceimport com.edurt.ssi.model.UserModeltrait UserService { /** * save model to db / def save(model: UserModel): UserModel;}创建UserServiceImpl实现类package com.edurt.ssi.serviceimport com.edurt.ssi.model.UserModelimport com.edurt.ssi.support.UserSupportimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Service@Service(value = “userService”)class UserServiceImpl @Autowired() ( val userSupport: UserSupport ) extends UserService { /* * save model to db / override def save(model: UserModel): UserModel = { return this.userSupport.save(model) }}创建用户UserController进行持久化数据package com.edurt.ssi.controllerimport com.edurt.ssi.model.UserModelimport com.edurt.ssi.service.UserServiceimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.web.bind.annotation.{PathVariable, PostMapping, RequestMapping, RestController}@RestController@RequestMapping(value = Array(“user”))class UserController @Autowired()( val userService: UserService ) { @PostMapping(value = Array(“save/{name}”)) def save(@PathVariable name: String): Long = { val userModel = { new UserModel() } userModel.name = name return this.userService.save(userModel).id }}使用控制台窗口执行以下命令保存数据curl -X POST http://localhost:8080/user/save/qianmoQ收到返回结果1表示数据保存成功增加数据读取渲染功能修改UserService增加以下代码/* * get all model /def getAll(page: Pageable): Page[UserModel]修改UserServiceImpl增加以下代码/* * get all model */override def getAll(page: Pageable): Page[UserModel] = { return this.userSupport.findAll(page)}修改UserController增加以下代码@GetMapping(value = Array(“list”))def get(): Page[UserModel] = this.userService.getAll(PageRequest.of(0, 10))创建UserView文件渲染User数据package com.edurt.ssi.viewimport com.edurt.ssi.service.UserServiceimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.data.domain.PageRequestimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMapping@Controllerclass UserView @Autowired()( private val userService: UserService ) { @GetMapping(value = Array(“user_view”)) def helloView(model: Model): String = { model.addAttribute(“users”, this.userService.getAll(PageRequest.of(0, 10))) return “user” }}创建user.mustache文件渲染数据(自行解析返回数据即可){{users}}浏览器访问http://localhost:8080/user_view即可看到页面内容增加单元功能修改pom.xml文件增加以下依赖<!– test –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> <exclusion> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> </exclusion> </exclusions></dependency><dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <scope>test</scope></dependency>创建UserServiceTest文件进行测试UserService功能package com.edurt.ssiimport com.edurt.ssi.service.UserServiceimport org.junit.jupiter.api.Testimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.boot.test.context.SpringBootTestimport org.springframework.data.domain.PageRequest@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class UserServiceTest @Autowired()( private val userService: UserService) { @Test def get all() { println(">> Assert blog page title, content and status code”) val entity = this.userService.getAll(PageRequest.of(0, 1)) print(entity.getTotalPages) }}源码地址:GitHub ...

March 1, 2019 · 2 min · jiezi

一致性 Hash 算法的实际应用

前言记得一年前分享过一篇《一致性 Hash 算法分析》,当时只是分析了这个算法的实现原理、解决了什么问题等。但没有实际实现一个这样的算法,毕竟要加深印象还得自己撸一遍,于是本次就当前的一个路由需求来着手实现一次。背景看过《为自己搭建一个分布式 IM(即时通讯) 系统》的朋友应该对其中的登录逻辑有所印象。先给新来的朋友简单介绍下 cim 是干啥的:其中有一个场景是在客户端登录成功后需要从可用的服务端列表中选择一台服务节点返回给客户端使用。而这个选择的过程就是一个负载策略的过程;第一版本做的比较简单,默认只支持轮询的方式。虽然够用,但不够优雅????。因此我的规划是内置多种路由策略供使用者根据自己的场景选择,同时提供简单的 API 供用户自定义自己的路由策略。先来看看一致性 Hash 算法的一些特点:构造一个 0 ~ 2^32-1 大小的环。服务节点经过 hash 之后将自身存放到环中的下标中。客户端根据自身的某些数据 hash 之后也定位到这个环中。通过顺时针找到离他最近的一个节点,也就是这次路由的服务节点。考虑到服务节点的个数以及 hash 算法的问题导致环中的数据分布不均匀时引入了虚拟节点。自定义有序 Map根据这些客观条件我们很容易想到通过自定义一个有序数组来模拟这个环。这样我们的流程如下:初始化一个长度为 N 的数组。将服务节点通过 hash 算法得到的正整数,同时将节点自身的数据(hashcode、ip、端口等)存放在这里。完成节点存放后将整个数组进行排序(排序算法有多种)。客户端获取路由节点时,将自身进行 hash 也得到一个正整数;遍历这个数组直到找到一个数据大于等于当前客户端的 hash 值,就将当前节点作为该客户端所路由的节点。如果没有发现比客户端大的数据就返回第一个节点(满足环的特性)。先不考虑排序所消耗的时间,单看这个路由的时间复杂度:最好是第一次就找到,时间复杂度为O(1)。最差为遍历完数组后才找到,时间复杂度为O(N)。理论讲完了来看看具体实践。我自定义了一个类:SortArrayMap他的使用方法及结果如下:可见最终会按照 key 的大小进行排序,同时传入 hashcode = 101 时会按照顺时针找到 hashcode = 1000 这个节点进行返回。下面来看看具体的实现。成员变量和构造函数如下:其中最核心的就是一个 Node 数组,用它来存放服务节点的 hashcode 以及 value 值。其中的内部类 Node 结构如下:写入数据的方法如下:相信看过 ArrayList 的源码应该有印象,这里的写入逻辑和它很像。写入之前判断是否需要扩容,如果需要则复制原来大小的 1.5 倍数组来存放数据。之后就写入数组,同时数组大小 +1。但是存放时是按照写入顺序存放的,遍历时自然不会有序;因此提供了一个 Sort 方法,可以把其中的数据按照 key 其实也就是 hashcode 进行排序。排序也比较简单,使用了 Arrays 这个数组工具进行排序,它其实是使用了一个 TimSort 的排序算法,效率还是比较高的。最后则需要按照一致性 Hash 的标准顺时针查找对应的节点:代码还是比较简单清晰的;遍历数组如果找到比当前 key 大的就返回,没有查到就取第一个。这样就基本实现了一致性 Hash 的要求。ps:这里并不包含具体的 hash 方法以及虚拟节点等功能(具体实现请看下文),这个可以由使用者来定,SortArrayMap 可作为一个底层的数据结构,提供有序 Map 的能力,使用场景也不局限于一致性 Hash 算法中。TreeMap 实现SortArrayMap 虽说是实现了一致性 hash 的功能,但效率还不够高,主要体现在 sort 排序处。下图是目前主流排序算法的时间复杂度:最好的也就是 O(N) 了。这里完全可以换一个思路,不用对数据进行排序;而是在写入的时候就排好顺序,只是这样会降低写入的效率。比如二叉查找树,这样的数据结构 jdk 里有现成的实现;比如 TreeMap 就是使用红黑树来实现的,默认情况下它会对 key 进行自然排序。来看看使用 TreeMap 如何来达到同样的效果。运行结果:127.0.0.1000效果和上文使用 SortArrayMap 是一致的。只使用了 TreeMap 的一些 API:写入数据候,TreeMap 可以保证 key 的自然排序。tailMap 可以获取比当前 key 大的部分数据。当这个方法有数据返回时取第一个就是顺时针中的第一个节点了。如果没有返回那就直接取整个 Map 的第一个节点,同样也实现了环形结构。ps:这里同样也没有 hash 方法以及虚拟节点(具体实现请看下文),因为 TreeMap 和 SortArrayMap 一样都是作为基础数据结构来使用的。性能对比为了方便大家选择哪一个数据结构,我用 TreeMap 和 SortArrayMap 分别写入了一百万条数据来对比。先是 SortArrayMap:耗时 2237 毫秒。TreeMap:耗时 1316毫秒。结果是快了将近一倍,所以还是推荐使用 TreeMap 来进行实现,毕竟它不需要额外的排序损耗。cim 中的实际应用下面来看看在 cim 这个应用中是如何具体使用的,其中也包括上文提到的虚拟节点以及 hash 算法。模板方法在应用的时候考虑到就算是一致性 hash 算法都有多种实现,为了方便其使用者扩展自己的一致性 hash 算法因此我定义了一个抽象类;其中定义了一些模板方法,这样大家只需要在子类中进行不同的实现即可完成自己的算法。AbstractConsistentHash,这个抽象类的主要方法如下:add 方法自然是写入数据的。sort 方法用于排序,但子类也不一定需要重写,比如 TreeMap 这样自带排序的容器就不用。getFirstNodeValue 获取节点。process 则是面向客户端的,最终只需要调用这个方法即可返回一个节点。下面我们来看看利用 SortArrayMap 以及 AbstractConsistentHash 是如何实现的。就是实现了几个抽象方法,逻辑和上文是一样的,只是抽取到了不同的方法中。只是在 add 方法中新增了几个虚拟节点,相信大家也看得明白。把虚拟节点的控制放到子类而没有放到抽象类中也是为了灵活性考虑,可能不同的实现对虚拟节点的数量要求也不一样,所以不如自定义的好。但是 hash 方法确是放到了抽象类中,子类不用重写;因为这是一个基本功能,只需要有一个公共算法可以保证他散列地足够均匀即可。因此在 AbstractConsistentHash 中定义了 hash 方法。这里的算法摘抄自 xxl_job,网上也有其他不同的实现,比如 FNV1_32_HASH 等;实现不同但是目的都一样。这样对于使用者来说就非常简单了:他只需要构建一个服务列表,然后把当前的客户端信息传入 process 方法中即可获得一个一致性 hash 算法的返回。同样的对于想通过 TreeMap 来实现也是一样的套路:他这里不需要重写 sort 方法,因为自身写入时已经排好序了。而在使用时对于客户端来说只需求修改一个实现类,其他的啥都不用改就可以了。运行的效果也是一样的。这样大家想自定义自己的算法时只需要继承 AbstractConsistentHash 重写相关方法即可,客户端代码无须改动。路由算法扩展性但其实对于 cim 来说真正的扩展性是对路由算法来说的,比如它需要支持轮询、hash、一致性hash、随机、LRU等。只是一致性 hash 也有多种实现,他们的关系就如下图:应用还需要满足对这一类路由策略的灵活支持,比如我也想自定义一个随机的策略。因此定义了一个接口:RouteHandlepublic interface RouteHandle { /** * 再一批服务器里进行路由 * @param values * @param key * @return */ String routeServer(List<String> values,String key) ;}其中只有一个方法,也就是路由方法;入参分别是服务列表以及客户端信息即可。而对于一致性 hash 算法来说也是只需要实现这个接口,同时在这个接口中选择使用 SortArrayMapConsistentHash 还是 TreeMapConsistentHash 即可。这里还有一个 setHash 的方法,入参是 AbstractConsistentHash;这就是用于客户端指定需要使用具体的那种数据结构。而对于之前就存在的轮询策略来说也是同样的实现 RouteHandle 接口。这里我只是把之前的代码搬过来了而已。接下来看看客户端到底是如何使用以及如何选择使用哪种算法。为了使客户端代码几乎不动,我将这个选择的过程放入了配置文件。如果想使用原有的轮询策略,就配置实现了 RouteHandle 接口的轮询策略的全限定名。如果想使用一致性 hash 的策略,也只需要配置实现了 RouteHandle 接口的一致性 hash 算法的全限定名。当然目前的一致性 hash 也有多种实现,所以一旦配置为一致性 hash 后就需要再加一个配置用于决定使用 SortArrayMapConsistentHash 还是 TreeMapConsistentHash 或是自定义的其他方案。同样的也是需要配置继承了 AbstractConsistentHash 的全限定名。不管这里的策略如何改变,在使用处依然保持不变。只需要注入 RouteHandle,调用它的 routeServer 方法。@Autowiredprivate RouteHandle routeHandle ;String server = routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId()));既然使用了注入,那其实这个策略切换的过程就在创建 RouteHandle bean 的时候完成的。也比较简单,需要读取之前的配置文件来动态生成具体的实现类,主要是利用反射完成的。这样处理之后就比较灵活了,比如想新建一个随机的路由策略也是同样的套路;到时候只需要修改配置即可。感兴趣的朋友也可提交 PR 来新增更多的路由策略。总结希望看到这里的朋友能对这个算法有所理解,同时对一些设计模式在实际的使用也能有所帮助。相信在金三银四的面试过程中还是能让面试官眼前一亮的,毕竟根据我这段时间的面试过程来看听过这个名词的都在少数????(可能也是和候选人都在 1~3 年这个层级有关)。以上所有源码:https://github.com/crossoverJie/cim如果本文对你有所帮助还请不吝转发。 ...

March 1, 2019 · 2 min · jiezi

SpringBoot 实战 (十三) | 整合 MyBatis (XML 版)

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 SpringBoot 与 Mybatis 的整合以及 Mybatis 的使用,之前介绍过了 SpringBoot 整合MyBatis 注解版的使用,上一篇介绍过 MyBatis 的理论,今天这篇就不介绍 MyBatis 的理论了,有兴趣的跳转阅读:SpringBoot 实战 (十三) | 整合 MyBatis (注解版)准备工作SpringBoot 2.1.3IDEAJDK 8创建表CREATE TABLE student ( id int(32) NOT NULL AUTO_INCREMENT, student_id varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘学号’, name varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘姓名’, age int(11) NULL DEFAULT NULL COMMENT ‘年龄’, city varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘所在城市’, dormitory varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘宿舍’, major varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘专业’, PRIMARY KEY (id) USING BTREE)ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8;引入依赖<dependencies> <!– jdbc 连接驱动 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!– web 启动类 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mybatis 依赖 –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!– druid 数据库连接池 –> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.14</version> </dependency> <!– Mysql 连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <!– 分页插件 –> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency> <!– test 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!– springboot maven 插件 –> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!– mybatis generator 自动生成代码插件 –> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> </plugin> </plugins> </build>代码解释很详细了,但这里提一嘴,mybatis generator 插件用于自动生成代码,pagehelper 插件用于物理分页。项目配置server: port: 8080spring: datasource: name: test url: jdbc:mysql://127.0.0.1:3306/test username: root password: 123456 #druid相关配置 type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select ‘x’ testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20## 该配置节点为独立的节点,有很多同学容易将这个配置放在spring的节点下,导致配置无法被识别mybatis: mapper-locations: classpath:mapping/*.xml #注意:一定要对应mapper映射xml文件的所在路径 type-aliases-package: com.nasus.mybatisxml.model # 注意:对应实体类的路径#pagehelper分页插件pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSqlmybatis generator 配置文件这里要注意,配置 pom.xml 中 generator 插件所对应的配置文件时,在 Pom.xml 加入这一句,说明 generator 插件所对应的配置文件所对应的配置文件路径。这里已经在 Pom 中配置了,请见上面的 Pom 配置。${basedir}/src/main/resources/generator/generatorConfig.xmlgeneratorConfig.xml :<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <!– 数据库驱动:选择你的本地硬盘上面的数据库驱动包–> <classPathEntry location=“D:\repository\mysql\mysql-connector-java\5.1.47\mysql-connector-java-5.1.47.jar”/> <context id=“DB2Tables” targetRuntime=“MyBatis3”> <commentGenerator> <property name=“suppressDate” value=“true”/> <!– 是否去除自动生成的注释 true:是 : false:否 –> <property name=“suppressAllComments” value=“true”/> </commentGenerator> <!–数据库链接URL,用户名、密码 –> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://127.0.0.1/test” userId=“root” password=“123456”> </jdbcConnection> <javaTypeResolver> <property name=“forceBigDecimals” value=“false”/> </javaTypeResolver> <!– 生成模型的包名和位置–> <javaModelGenerator targetPackage=“com.nasus.mybatisxml.model” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> <property name=“trimStrings” value=“true”/> </javaModelGenerator> <!– 生成映射文件的包名和位置–> <sqlMapGenerator targetPackage=“mapping” targetProject=“src/main/resources”> <property name=“enableSubPackages” value=“true”/> </sqlMapGenerator> <!– 生成DAO的包名和位置–> <javaClientGenerator type=“XMLMAPPER” targetPackage=“com.nasus.mybatisxml.mapper” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> </javaClientGenerator> <!– 要生成的表 tableName是数据库中的表名或视图名 domainObjectName是实体类名–> <table tableName=“student” domainObjectName=“Student” enableCountByExample=“false” enableUpdateByExample=“false” enableDeleteByExample=“false” enableSelectByExample=“false” selectByExampleQueryId=“false”></table> </context></generatorConfiguration>代码注释很详细,不多说。生成代码过程第一步:选择编辑配置第二步:选择添加 Maven 配置第三步:添加命令 mybatis-generator:generate -e 点击确定第四步:运行该配置,生成代码特别注意!!!同一张表一定不要运行多次,因为 mapper 的映射文件中会生成多次的代码,导致报错,切记。如要运行多次,请把上次生成的 mapper 映射文件代码删除再运行。第五步:检查生成结果遇到的问题请参照别人写好的遇到问题的解决方法,其中我就遇到数据库时区不对以及只生成 Insert 方法这两个问题。都是看以下这篇文章解决的:Mybatis Generator自动生成代码以及可能出现的问题生成的代码1、实体类:Student.javapackage com.nasus.mybatisxml.model;public class Student { private Long id; private Integer age; private String city; private String dormitory; private String major; private String name; private Long studentId; // 省略 get 和 set 方法}2、mapper 接口:StudentMapper.javapackage com.nasus.mybatisxml.mapper;import com.nasus.mybatisxml.model.Student;import java.util.List;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface StudentMapper { int deleteByPrimaryKey(Long id); int insert(Student record); int insertSelective(Student record); Student selectByPrimaryKey(Long id); // 我添加的方法,相应的要在映射文件中添加此方法 List<Student> selectStudents(); int updateByPrimaryKeySelective(Student record); int updateByPrimaryKey(Student record);}3、映射文件:StudentMapper.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=“com.nasus.mybatisxml.mapper.StudentMapper” > <resultMap id=“BaseResultMap” type=“com.nasus.mybatisxml.model.Student” > <id column=“id” property=“id” jdbcType=“BIGINT” /> <result column=“age” property=“age” jdbcType=“INTEGER” /> <result column=“city” property=“city” jdbcType=“VARCHAR” /> <result column=“dormitory” property=“dormitory” jdbcType=“VARCHAR” /> <result column=“major” property=“major” jdbcType=“VARCHAR” /> <result column=“name” property=“name” jdbcType=“VARCHAR” /> <result column=“student_id” property=“studentId” jdbcType=“BIGINT” /> </resultMap> <sql id=“Base_Column_List” > id, age, city, dormitory, major, name, student_id </sql> <select id=“selectByPrimaryKey” resultMap=“BaseResultMap” parameterType=“java.lang.Long” > select <include refid=“Base_Column_List” /> from student where id = #{id,jdbcType=BIGINT} </select> <delete id=“deleteByPrimaryKey” parameterType=“java.lang.Long” > delete from student where id = #{id,jdbcType=BIGINT} </delete> <insert id=“insert” parameterType=“com.nasus.mybatisxml.model.Student” > insert into student (id, age, city, dormitory, major, name, student_id) values (#{id,jdbcType=BIGINT}, #{age,jdbcType=INTEGER}, #{city,jdbcType=VARCHAR}, #{dormitory,jdbcType=VARCHAR}, #{major,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{studentId,jdbcType=BIGINT}) </insert> <insert id=“insertSelective” parameterType=“com.nasus.mybatisxml.model.Student” > insert into student <trim prefix=”(” suffix=")" suffixOverrides="," > <if test=“id != null” > id, </if> <if test=“age != null” > age, </if> <if test=“city != null” > city, </if> <if test=“dormitory != null” > dormitory, </if> <if test=“major != null” > major, </if> <if test=“name != null” > name, </if> <if test=“studentId != null” > student_id, </if> </trim> <trim prefix=“values (” suffix=")" suffixOverrides="," > <if test=“id != null” > #{id,jdbcType=BIGINT}, </if> <if test=“age != null” > #{age,jdbcType=INTEGER}, </if> <if test=“city != null” > #{city,jdbcType=VARCHAR}, </if> <if test=“dormitory != null” > #{dormitory,jdbcType=VARCHAR}, </if> <if test=“major != null” > #{major,jdbcType=VARCHAR}, </if> <if test=“name != null” > #{name,jdbcType=VARCHAR}, </if> <if test=“studentId != null” > #{studentId,jdbcType=BIGINT}, </if> </trim> </insert> <update id=“updateByPrimaryKeySelective” parameterType=“com.nasus.mybatisxml.model.Student” > update student <set > <if test=“age != null” > age = #{age,jdbcType=INTEGER}, </if> <if test=“city != null” > city = #{city,jdbcType=VARCHAR}, </if> <if test=“dormitory != null” > dormitory = #{dormitory,jdbcType=VARCHAR}, </if> <if test=“major != null” > major = #{major,jdbcType=VARCHAR}, </if> <if test=“name != null” > name = #{name,jdbcType=VARCHAR}, </if> <if test=“studentId != null” > student_id = #{studentId,jdbcType=BIGINT}, </if> </set> where id = #{id,jdbcType=BIGINT} </update> <update id=“updateByPrimaryKey” parameterType=“com.nasus.mybatisxml.model.Student” > update student set age = #{age,jdbcType=INTEGER}, city = #{city,jdbcType=VARCHAR}, dormitory = #{dormitory,jdbcType=VARCHAR}, major = #{major,jdbcType=VARCHAR}, name = #{name,jdbcType=VARCHAR}, student_id = #{studentId,jdbcType=BIGINT} where id = #{id,jdbcType=BIGINT} </update> <!– 我添加的方法 –> <select id=“selectStudents” resultMap=“BaseResultMap”> SELECT <include refid=“Base_Column_List” /> from student </select></mapper>serviec 层1、接口:public interface StudentService { int addStudent(Student student); Student findStudentById(Long id); PageInfo<Student> findAllStudent(int pageNum, int pageSize);}2、实现类@Servicepublic class StudentServiceImpl implements StudentService{ //会报错,不影响 @Resource private StudentMapper studentMapper; /** * 添加学生信息 * @param student * @return / @Override public int addStudent(Student student) { return studentMapper.insert(student); } /* * 根据 id 查询学生信息 * @param id * @return / @Override public Student findStudentById(Long id) { return studentMapper.selectByPrimaryKey(id); } /* * 查询所有学生信息并分页 * @param pageNum * @param pageSize * @return */ @Override public PageInfo<Student> findAllStudent(int pageNum, int pageSize) { //将参数传给这个方法就可以实现物理分页了,非常简单。 PageHelper.startPage(pageNum, pageSize); List<Student> studentList = studentMapper.selectStudents(); PageInfo result = new PageInfo(studentList); return result; }}controller 层@RestController@RequestMapping("/student")public class StudentController { @Autowired private StudentService studentService; @GetMapping("/{id}") public Student findStidentById(@PathVariable(“id”) Long id){ return studentService.findStudentById(id); } @PostMapping("/add") public int insertStudent(@RequestBody Student student){ return studentService.addStudent(student); } @GetMapping("/list") public PageInfo<Student> findStudentList(@RequestParam(name = “pageNum”, required = false, defaultValue = “1”) int pageNum, @RequestParam(name = “pageSize”, required = false, defaultValue = “10”) int pageSize){ return studentService.findAllStudent(pageNum,pageSize); }}启动类@SpringBootApplication@MapperScan(“com.nasus.mybatisxml.mapper”) // 扫描 mapper 接口,必须加上public class MybatisxmlApplication { public static void main(String[] args) { SpringApplication.run(MybatisxmlApplication.class, args); }}提一嘴,@MapperScan(“com.nasus.mybatisxml.mappe”) 这个注解非常的关键,这个对应了项目中 mapper(dao) 所对应的包路径,必须加上,否则会导致异常。Postman 测试1、插入方法:2、根据 id 查询方法:3、分页查询方法:源码下载https://github.com/turoDog/De…帮忙点个 star 可好?后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 1, 2019 · 5 min · jiezi

SpringBoot+jsp项目启动出现404

通过maven创建springboot项目启动出现404application.properties配置spring.mvc.view.prefix=/WEB-INF/jsp/spring.mvc.view.suffix=.jsp项目结构控制器方法package com.example.demo.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class IndexController { @RequestMapping("/") public String index() { return “index”; }}启动项目访问localhost:8080,出现404Whitelabel Error PageThis application has no explicit mapping for /error, so you are seeing this as a fallback.Thu Feb 28 22:59:29 CST 2019There was an unexpected error (type=Not Found, status=404).No message available解决方法pom.xml添加依赖<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId></dependency><dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId></dependency>clean并刷新maven重启并访问localhost:8080

March 1, 2019 · 1 min · jiezi

sorl实现快速搜索

Docker安装Solr服务拉取 Solr 镜像:docker pull solr:7.4.0启动 Solr 容器docker run –name taotao-solr -d -p 8983:8983 -t solr:7.4.0访问 http://ip:8983/ Solr界面功

February 28, 2019 · 1 min · jiezi

spring boot 整合mybatis 无法输出sql的问题

使用spring boot整合mybatis,测试功能的时候,遇到到了sql问题,想要从日志上看哪里错了,但是怎么都无法输出执行的sql,我使用的是log4j2,百度了一下,很多博客都说,加上下面的日志配置: <logger name=“java.sql.Statement” level=“debug”/> <logger name=“java.sql.PreparedStatement” level=“debug”/> <logger name=“java.sql.Connection” level=“debug”/> <logger name=“ResultSet” level=“debug”/>经实际测试,没什么用。只好去官网找解决方案,在mybatis日志上看到,如果存在内置日志,就是用内置的日志,自己配置的日志就忽略了。MyBatis 内置日志工厂基于运行时自省机制选择合适的日志工具。它会使用第一个查找得到的工具(按上文列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。 不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging,所以在这种配置环境下的 MyBatis 会把它作为日志工具,记住这点非常重要。这将意味着,在诸如 WebSphere 的环境中,它提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。MyBatis 将你的 Log4J 配置忽略掉是相当令人郁闷的(事实上,正是因为在这种配置环境下,MyBatis 才会选择使用 Commons Logging 而不是 Log4J)。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志工具,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择别的日志工具。<configuration> <settings> … <setting name=“logImpl” value=“LOG4J”/> … </settings></configuration> logImpl 可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了接口 org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名。(译者注:可以参考org.apache.ibatis.logging.slf4j.Slf4jImpl.java的实现)所以我就创建了一个mybatis-config文件:<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE configuration PUBLIC “-//mybatis.org//DTD Config 3.0//EN” “http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <settings> <setting name=“logImpl” value=“LOG4J2”/> </settings></configuration>配置mybatis:mybatis: type-aliases-package: com.demo mapper-locations: classpath:mapper/.xml configuration: cache-enabled: true lazy-loading-enabled: true multiple-result-sets-enabled: true default-executor-type: simple default-statement-timeout: 25000 config-location: classpath:mybatis-config.xml然而启动的时候报错:aused by: java.lang.IllegalStateException: Property ‘configuration’ and ‘configLocation’ can not specified with together at org.springframework.util.Assert.state(Assert.java:73) ~[spring-core-5.0.10.RELEASE.jar:5.0.10.RELEASE] at org.mybatis.spring.SqlSessionFactoryBean.afterPropertiesSet(SqlSessionFactoryBean.java:377) ~[mybatis-spring-1.3.2.jar:1.3.2] at org.mybatis.spring.SqlSessionFactoryBean.getObject(SqlSessionFactoryBean.java:547) ~[mybatis-spring-1.3.2.jar:1.3.2] at org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.sqlSessionFactory(MybatisAutoConfiguration.java:153) ~[mybatis-spring-boot-autoconfigure-1.3.2.jar:1.3.2] at org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration$$EnhancerBySpringCGLIB$$8eb42bd5.CGLIB$sqlSessionFactory$0(<generated>) ~[mybatis-spring-boot-autoconfigure-1.3.2.jar:1.3.2] at org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration$$EnhancerBySpringCGLIB$$8eb42bd5$$FastClassBySpringCGLIB$$1132516e.invoke(<generated>) ~[mybatis-spring-boot-autoconfigure-1.3.2.jar:1.3.2] 报错信息说configuration和configLocation不能同时存在,所以我就想两个功能一定是相同的,只不过配置方式不一样,一个是xml,一个是spring boot的风格,所以我就删掉了config-location,在configuration下面找到了一个log-impl,加上这个配置之后,问题就解决了,如下:mybatis: type-aliases-package: com.demo mapper-locations: classpath:mapper/.xml configuration: cache-enabled: true lazy-loading-enabled: true multiple-result-sets-enabled: true default-executor-type: simple default-statement-timeout: 25000 log-impl: org.apache.ibatis.logging.log4j2.Log4j2Impl输出日志: ==> Preparing: select prodid,prodname,isgroupinsur,isdtbprod from bx_product where prodid in ( 1 , 2 , 3 ) ==> Parameters: <== Total: 0只要加这个配置就能解决问题了。 ...

February 28, 2019 · 1 min · jiezi

自制一个 elasticsearch-spring-boot-starter

概 述Elasticsearch 在企业里落地的场景越来越多了,但是大家在项目里使用 Elasticsearch的姿势也是千奇百怪,这次正好自己需要使用,所以干脆就封装一个 elasticsearch-spring-boot-starter以供复用好了。如果不知道 spring-boot-starter该如何制作,可以参考文章《如何自制一个Spring Boot Starter并推送到远端公服》,下面就来简述一下自制的 elasticsearch-spring-boot-starter该如何使用。依赖引入<dependency> <groupId>com.github.hansonwang99</groupId> <artifactId>elasticsearch-spring-boot-starter</artifactId> <version>0.0.8</version></dependency><repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository></repositories>配置文件如果你还没有一个属于自己的 Elasticsearch集群,可以参考文章 《CentOS7 上搭建多节点 Elasticsearch集群》来一步步搭建之,本文实验所用的集群即来源于此。elasticsearch: host: 192.168.31.75 httpPort: 9200 tcpPort: 9300 clusterName: codesheep docFields: title,filecontent auth: enable: false各个字段解释如下:host:Elasticsearch 节点地址httpPort: Elasticsearch REST端口tcpPort:Elasticsearch TCP端口clusterName:集群名docFields:文档字段,以英文逗号间隔,比如我这里的业务场景是文档包含 标题(title)和 内容(filecontent)字段auth:是否需要权限认证由于我这里安装的实验集群并无 x-pack权限认证的加持,因此无需权限认证,实际使用的集群或者阿里云上的 Elasticsearch集群均有完善的 x-pack权限认证,此时可以加上用户名/密码的配置:elasticsearch: host: 192.168.199.75 httpPort: 9200 tcpPort: 9300 clusterName: codesheep docFields: title,filecontent auth: enable: true username: elasticsearch password: xxxxxx用法例析首先注入相关资源@Autowiredprivate ISearchService iSearchService;@Autowiredprivate DocModel docModel;这些都是在 elasticsearch-spring-boot-starter中定义的创建索引public String createIndex() throws IOException { IndexModel indexModel = new IndexModel(); indexModel.setIndexName(“testindex2”); // 注意索引名字必须小写,否则ES抛异常 indexModel.setTypeName(“testtype2”); indexModel.setReplicaNumber( 2 ); // 两个节点,因此两个副本 indexModel.setShardNumber( 3 ); XContentBuilder builder = null; builder = XContentFactory.jsonBuilder(); builder.startObject(); { builder.startObject(“properties”); { builder.startObject(“title”); { builder.field(“type”, “text”); builder.field(“analyzer”, “ik_max_word”); } builder.endObject(); builder.startObject(“filecontent”); { builder.field(“type”, “text”); builder.field(“analyzer”, “ik_max_word”); builder.field(“term_vector”, “with_positions_offsets”); } builder.endObject(); } builder.endObject(); } builder.endObject(); indexModel.setBuilder( builder ); Boolean res = iSearchService.createIndex(indexModel); if( true==res ) return “创建索引成功”; else return “创建索引失败”;}删除索引public String deleteIndex() { return (iSearchService.deleteIndex(“testindex2”)==true) ? “删除索引成功”:“删除索引失败”;}判断索引是否存在if ( existIndex(indexName) ) { …} else { …}插入单个文档public String insertSingleDoc( ) { SingleDoc singleDoc = new SingleDoc(); singleDoc.setIndexName(“testindex2”); singleDoc.setTypeName(“testtype2”); Map<String,Object> doc = new HashMap<>(); doc.put(“title”,“人工智能标题1”); doc.put(“filecontent”,“人工智能内容1”); singleDoc.setDocMap(doc); return ( true== iSearchService.insertDoc( singleDoc ) ) ? “插入单个文档成功” : “插入单个文档失败”;}批量插入文档public String insertDocBatch() { BatchDoc batchDoc = new BatchDoc(); batchDoc.setIndexName(“testindex2”); batchDoc.setTypeName(“testtype2”); Map<String,Object> doc1 = new HashMap<>(); doc1.put(“title”,“人工智能标题1”); doc1.put(“filecontent”,“人工智能内容1”); Map<String,Object> doc2 = new HashMap<>(); doc2.put(“title”,“人工智能标题2”); doc2.put(“filecontent”,“人工智能内容2”); Map<String,Object> doc3 = new HashMap<>(); doc3.put(“title”,“人工智能标题3”); doc3.put(“filecontent”,“人工智能内容3”); Map<String,Object> doc4 = new HashMap<>(); doc4.put(“title”,“人工智能标题4”); doc4.put(“filecontent”,“人工智能内容4”); List<Map<String,Object>> docList = new ArrayList<>(); docList.add( doc1 ); docList.add( doc2 ); docList.add( doc3 ); docList.add( doc4 ); batchDoc.setBatchDocMap( docList ); return ( true== iSearchService.insertDocBatch( batchDoc ) ) ? “批量插入文档成功” : “批量插入文档失败”;}搜索文档public List<Map<String,Object>> searchDoc() { SearchModel searchModel = new SearchModel(); searchModel.setIndexName( “testindex2” ); List<String> fields = new ArrayList<>(); fields.add(“title”); fields.add(“filecontent”); fields.add(“id”); searchModel.setFields( fields ); searchModel.setKeyword( “人工” ); searchModel.setPageNum( 1 ); searchModel.setPageSize( 5 ); return iSearchService.queryDocs( searchModel );}删除文档public String deleteDoc() { SingleDoc singleDoc = new SingleDoc(); singleDoc.setIndexName(“testindex2”); singleDoc.setTypeName(“testtype2”); singleDoc.setId(“vPHMY2cBcGZ3je_1EgIM”); return (true== iSearchService.deleteDoc(singleDoc)) ? “删除文档成功” : “删除文档失败”;}批量删除文档public String deleteDocBatch() { BatchDoc batchDoc = new BatchDoc(); batchDoc.setIndexName(“testindex2”); batchDoc.setTypeName(“testtype2”); List<String> ids = new ArrayList<>(); ids.add(“vfHMY2cBcGZ3je_1EgIM”); ids.add(“vvHMY2cBcGZ3je_1EgIM”); batchDoc.setDocIds( ids ); return ( true== iSearchService.deleteDocBatch(batchDoc) ) ? “批量删除文档成功” : “批量删除文档失败”;}更新文档public String updateDoc( @RequestBody SingleDoc singleDoc ) { SingleDoc singleDoc = new SingleDoc(); singleDoc.setId(“wPH6Y2cBcGZ3je_1OwI7”); singleDoc.setIndexName(“testindex2”); singleDoc.setTypeName(“testtype2”); Map<String,Object> doc = new HashMap<>(); doc.put(“title”,“人工智能标题(更新后)”); doc.put(“filecontent”,“人工智能内容(更新后)”); singleDoc.setUpdateDocMap(doc); return (true== iSearchService.updateDoc(singleDoc)) ? “更新文档成功” : “更新文档失败”;}后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

February 28, 2019 · 2 min · jiezi

上手spring cloud(二)微应用之间的服务调用

微应用之间的服务调用服务调用示例以商品下单为例,比如将业务拆分为商品服务和订单服务,订单服务会调用商品服务的库存扣减。单个微服务工程,统一按以下目录编排:-product–product-common 商品服务公用对象–product-client 商品服务客户端,以jar包方式被订单服务依赖–product-server 商品服务,要注册到Eureka Server,外部通过product-client来调用我们的微服务工程之间,依赖关系如下:我们从商品微服务工程开始外层product初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/></parent><groupId>com.hicoview</groupId><artifactId>product</artifactId><version>0.0.1-SNAPSHOT</version><name>product</name><description>Demo project for Spring Boot</description><modules> <module>product-common</module> <module>product-client</module> <module>product-server</module></modules><properties> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> <product-common.version>0.0.1-SNAPSHOT</product-common.version></properties><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> <dependency> <groupId>com.hicoview</groupId> <artifactId>product-common</artifactId> <version>${product-common.version}</version> </dependency> </dependencies></dependencyManagement>product-common初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>com.hicoview</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version></parent><artifactId>product-common</artifactId><dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency></dependencies>product-client初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>com.hicoview</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version></parent><artifactId>product-client</artifactId><dependencies> <dependency> <groupId>com.hicoview</groupId> <artifactId>product-common</artifactId> </dependency></dependencies>product-server的初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>com.hicoview</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version></parent><artifactId>product-server</artifactId><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.hicoview</groupId> <artifactId>product-common</artifactId> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build>接下来我们调整product-server工程商品服务需注册到Eureka Server,添加Eureka Client相关依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>启动类加注解@EnableDiscoveryClient:package com.hicoview.product;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); }}配置application.yml:eureka: client: service-url: defaultZone: http://localhost:8761/eureka/spring: application: name: productserver: port: 8080启动product-server,访问注册中心http://localhost:8761,PRODUCT注册上去了,再继续往下看。我们写个简单的服务调用示例一下,订单服务下单逻辑调用商品服务进行扣减库存。继续修改product-server工程package com.hicoview.product.service;import java.util.List;public interface ProductService { // 扣减库存 void decreaseStock();}package com.hicoview.product.service.impl;import com.hicoview.product.service.ProductService;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;@Service@Slf4jpublic class ProductServiceImpl implements ProductService { @Override public void decreaseStock() { log.info("——扣减库存—–"); }}spring cloud的RPC服务是使用HTTP方式调用的,所以还要创建ProductController:package com.hicoview.product.controller;import com.hicoview.product.service.ProductService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/product")public class ProductController { @Autowired private ProductService productService; @PostMapping("/decreaseStock") public void decreaseStock() { productService.decreaseStock(); }}接下来修改product-client工程pom.xml增加Feign依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>创建ProductClient:package com.hicoview.product.client;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;@FeignClient(name=“product”)public interface ProductClient { // 通过Feign来代理对PRODUCT服务的HTTP请求 @PostMapping("/product/decreaseStock") void decreaseStock();}再次启动product-server,没问题的话,把product上传到本地maven仓库:mvn -Dmaven.test.skip=true -U clean install然后初始化订单微服务工程后继续。修改order-server工程order-server的服务可能会被User等其他服务调用,也是个Eureka client,添加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>启动类加注解@EnableDiscoveryClientpackage com.hicoview.product;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); }}配置application.yml:eureka: client: service-url: defaultZone: http://localhost:8761/eureka/spring: application: name: orderserver: port: 8081启动order-server,访问注册中心http://localhost:8761/:ORDER服务注册成功后,继续调整order-server工程由于订单服务要调用商品服务,需添加对product-client依。修改pom.xml:<dependency> <groupId>com.hicoview</groupId> <artifactId>product-client</artifactId></dependency>对版本的管理统一交给上层,修改上层order的pom.xml,增加以下配置:<properties> … <product-client.version>0.0.1-SNAPSHOT</product-client.version></properties><dependencyManagement> <dependencies> … <dependency> <groupId>com.hicoview</groupId> <artifactId>product-client</artifactId> <version>${product-client.version}</version> </dependency> </dependencies></dependencyManagement>回到order-server,修改启动类,添加Feign扫描路径:package com.hicoview.order;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableDiscoveryClient@EnableFeignClients(basePackages = “com.hicoview.product.client”)public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); }}然后按顺序创建Controller、Service:package com.hicoview.order.controller;import com.hicoview.order.service.OrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/order")public class OrderController { @Autowired private OrderService orderService; // 创建订单 @PostMapping("/create") public void create() { orderService.createOrder(); }}public interface OrderService { void createOrder();}@Service@Slf4jpublic class OrderServiceImpl implements OrderService { @Autowired private ProductClient productClient; @Override public void createOrder() { log.info("——创建订单—–"); // 调用商品扣减服务 productClient.decreaseStock(); }}启动order-server,发起下单请求:curl -X POST http://localhost:8081/order/create服务调用成功,order-server和product-server输出:2019-02-26 11:09:58.408 INFO 3021 — [nio-8081-exec-3] c.h.order.service.impl.OrderServiceImpl : ——创建订单—–2019-02-26 11:09:58.430 INFO 3015 — [nio-8080-exec-3] c.h.p.service.impl.ProductServiceImpl : ——扣减库存—–为了示例的极简,上面服务调用没有涉及到入参和返回值。如果需要定义参数或返回值,考虑到内外部都会用到,需将参数bean定义在product-common中作为公共bean。Feign和RestTemplateRest服务的调用,可以使用Feign或RestTemplate来完成,上面示例我们使用了Feign。Feign(推荐)Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。Feign会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign整合了Ribbon和Hystrix(关于Hystrix我们后面再讲),可以让我们不再需要显式地使用这两个组件。RestTemplateRestTemplate提供了多种便捷访问远程Http服务的方法,可以了解下。第一种方式,直接使用RestTemplate,URL写死:// 1. RestTemplate restTemplate = new RestTemplate();restTemplate.getForObject(“http://localhost:8080/product/decreaseStock”, String.class);这种方式直接使用目标的IP,而线上部署的IP地址可能会切换,同一个服务还会启多个进程,所以有弊端。第二种方式,利用LoadBalancerClient,通过应用名获取URL,然后再使用RestTemplate:@Autowiredprivate LoadBalancerClient loadBalancerClient;// 1. 第二种方式,通过应用名字拿到其中任意一个host和portServiceInstance serviceInstance = loadBalancerClient.choose(“PRODUCT”);String url = String.format(“http://%s:%s”, serviceInstance.getHost(), serviceInstance.getPort() + “/product/decreaseStock”);RestTemplate restTemplate = new RestTemplate();restTemplate.getForObject(url, String.class);第三种方式,写一个config把RestTemplate作为一个bean配置上去:@Componentpublic class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }}@Autowiredprivate RestTemplate restTemplate;// 使用时url里直接用应用名PRODUCT即可restTemplate.getForObject(“http://PRODUCT/product/decreaseStock”, String.class);Ribbon客户端负载均衡器Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。在Spring Cloud中,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。还有微服务之间的调用,通过Feign或RestTemplate找到一个目标服务。以及API网关Zuul的请求转发等内容,实际上都是通过Ribbon来实现的。 ...

February 27, 2019 · 2 min · jiezi

上手spring cloud(三)统一配置中心

统一配置中心Spring Cloud Config为各应用环境提供了一个中心化的外部配置。配置服务器默认采用git来存储配置信息,这样就有助于对配置进行版本管理,并且可以通过git客户端工具来方便维护配置内容。当然它也提供本地化文件系统的存储方式。使用集中式配置管理,在配置变更时,可以通知到各应用程序,应用程序不需要重启。Config Server创建Config Server端工程config-server:File -> New->Product… -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next(springboot选择2.0以上)选择Cloud Discovery -> 选择Eureka Discovery选择Cloud Config -> 选择Config Server由于选择了Eureka Discovery和Config Server,创建成功后pom.xml里已经帮你引入了以下依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>Config Server也是要注册到Eureka,作为Eureka Client,我们还要加入如下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><!– 避免后面的数据库配置出错,mysql依赖也加了 –><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency>再给启动类加上注解@EnableDiscoveryClient和@EnableConfigServer:package com.hicoview.config;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.config.server.EnableConfigServer;@SpringBootApplication@EnableDiscoveryClient@EnableConfigServerpublic class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); }}配置application.yml:eureka: client: service-url: defaultZone: http://localhost:8761/eureka/spring: application: name: config cloud: config: server: git: uri: http://code.hicoview.com:8000/backend/config.git username: root password: 8ggf9afd6g9gj # 配置文件下载后存储的本地目录 basedir: /Users/zhutx/springcloud/config/basedirserver: port: 9999然后按照配置的git仓库地址,在github或gitlab上创建config仓库。以商品微服务的配置来演示,在config仓库创建product-dev.yml:server: port: 8080spring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: passwd_1986 url: jdbc:mysql://127.0.0.1:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false启动作为Config Server的config-server工程,查看http://localhost:8761:访问配置服务端的以下任意地址,都可以显示出对应格式的配置内容:http://localhost:9999/product-dev.ymlhttp://localhost:9999/product-dev.propertieshttp://localhost:9999/product-dev.json可见,Config Server获取到了远程git仓库上的配置,并将其作为自身的REST服务提供了出去。接下来我们看看配置客户端Config Client(即product-server)怎么引用配置。Config Client我们给product-server加入配置客户端的依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId></dependency>修改application.yml配置:spring: application: name: product cloud: config: discovery: enabled: true service-id: CONFIG profile: deveureka: client: service-url: defaultZone: http://localhost:8761/eureka/这样子就可以从Eureka服务注册中心找到CONFIG服务,并拿到product-dev.yml了。启动product-server,查看eureka注册中心,CONFIG这个Config Server服务已经注册上去了:如果代码里有操作数据库,那么启动其实会出错,因为spring boot不知道配置加载顺序。我们期望先拿到CONFIG的配置,再初始化数据库。解决办法是把product-server的application.yml改成bootstrap.yml就好。微服务工程使用配置服务的情况下,注意将application.yml都改成bootstrap.yml。并且,让bootstrap.yml文件只保留Eureka配置和获取Config Server服务的配置;另外,如果生产环境要使用统一配置中心,可以启动多个Config Server进程,保持高可用。同样的操作,把order-server配置也抽取到外部Spring Cloud Bus下图是当前的配置工作机制,config-server拉取远端git配置,并在本地存一份。然后config-server通过把自身注册到Eureka从而提供了拉取配置的服务,而配置客户端(product和order)通过引入config-client依赖,在启动时便能获取加载到了配置。我们需要做到修改远程配置,应用程序不重启,还需要借助Spring Cloud Bus。Spring Cloud Bus集成了MQ,并为config-server提供了这个配置刷新服务(bus-refresh)。如下图所示,做法是远端git修改配置后,通过webhook调用config-server的/bus-refresh服务,发布RabbitMQ消息,config-client接收消息并更新配置。我们先安装RabbitMQ:# docker安装rabbitmqdocker run -d –hostname my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.7.9-management# 验证下docker ps | grep ‘rabbitmq’能成功访问RabbitMQ控制台http://localhost:15671,继续。修改Config Server端,增加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>修改application.yml,增加以下配置,把包括bus-refresh在内的所有config server的服务都暴露出来:management: endpoints: web: exposure: include: “*“我们拿product-server来演示,修改product-server的pom增加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>然后提供一个接口,方便我们在配置变验证结果:@RestController@RequestMapping("/env”)@RefreshScopepublic class EnvController { @Value("${env}”) private String env; @GetMapping("/print") public String print() { return env; }}测试下,我们修改远端git配置,先增加env配置:server: port: 8080spring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: passwd_1986 url: jdbc:mysql://127.0.0.1:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false# 增加了该配置env: dev重启下config-server和product-server。访问 product-server http://localhost:8080/env/print,显示dev然后我们把远端env配置项改成test调用config-server的配置刷新服务 bus-refresh:curl -v -X POST http://localhost:9999/actuator/bus-refresh再次访问 product-server http://localhost:8080/env/print,显示test至此,已经做到了变更配置不重启应用。我们再借助Git仓库的webhook功能,在push指令发生后帮我们发个bus-refresh请求就完美了。Gitlab的话在仓库的这个位置:Repository -> Settings -> Integrations -> Add webhook ...

February 27, 2019 · 1 min · jiezi

上手spring cloud(一)Eureka服务注册与发现

spring cloud面向开发人员,对分布式系统从编程模型上提供了强大的支持。可以说是分布式系统解决方案的全家桶,极大地降低了开发与构建分布式系统的门槛。包括了诸如下列功能:Eureka服务注册发现统一配置中心Spring Cloud Stream异步调用Zuul服务网关Hystrix服务降级容错服务调用跟踪Netflix是一家互联网流媒体播放商,美国视频巨头,最近买下《流浪地球》,并在190多个国家播出的就是它了。随着Netflix转型为一家云计算公司,也开始积极参与开源项目。Netflix OSS(Open Source)就是由Netflix公司开发的框架,解决上了规模之后的分布式系统可能出现的一些问题。spring cloud基于spring boot,为spring boot提供Netflix OSS的集成。这段时间对spring cloud进行了学习总结,留下点什么,好让网友上手spring cloud时少躺坑。我用的版本如下:spring-boot 2.0.2.RELEASEspring-cloud Finchley.RELEASEspring boot不同版本之间在配置和依赖上会有差异,强烈建议创建工程后先把spring boot和spring cloud的版本依赖调整成与本篇一致,快速上手少躺坑。上手完后,你可以尝试升到最新版本,官方spring-cloud页(进去后拉到最下面)提供了spring-cloud与spring-boot版本匹配表。升级完后建议重新测试一下相关功能。Eureka服务注册与发现Eureka Server服务注册中心IntelliJ IDEA(我是2018.2.5的版本),创建工程eureka-server:File -> New->Product… -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next -> 选择Cloud Discovery -> 选择Eureka Server注意创建工程后调整spring boot和spring cloud的版本:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent>…<properties> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version></properties>由于选择了Eureka Server,创建成功后pom.xml里已经帮你引入了以下依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>我们给启动类加上注解@EnableEurekaServer:package com.hicoview.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); }}配置application.yml(习惯yml风格的同学,把application.properties直接改过来):eureka: client: # 默认eureka服务注册中心会将自身作为客户端来尝试注册,所以我们需要禁用它的客户端注册行为 register-with-eureka: false # 默认30秒会更新客户端注册上来的服务清单,启动时就不获取了,不然启动会有报错,虽然不影响 fetch-registry: false server: # 关闭注册中心自我保护(默认是true,生产环境不建议关闭,去掉该配置项或改成true) enable-self-preservation: falsespring: application: name: eurekaserver: port: 8761服务注册中心搞定,启动成功后访问http://localhost:8761,可以看到注册中心页面:Eureka Server高可用只要启动多个注册中心进程即可,多个之间,两两注册到对方。比如,8761端口启动一个eueka服务端,注册到8762:eureka: client: service-url: # 提供其他注册中心的地址,注册中心将自身以客户端注册的方式注册到其他注册中心去 defaultZone: http://localhost:8762/eureka/ register-with-eureka: false fetch-registry: false server: enable-self-preservation: falsespring: application: name: eurekaserver: port: 87618762端口启动一个eueka服务端,注册到8761:eureka: client: service-url: # 提供其他注册中心的地址,注册中心将自身以客户端注册的方式注册到其他注册中心去 defaultZone: http://localhost:8761/eureka/ register-with-eureka: false fetch-registry: false server: enable-self-preservation: falsespring: application: name: eurekaserver: port: 8762访问http://localhost:8761,可以看到8761这里有了8762的副本:访问http://localhost:8762,也是一样,你可以试试。接下来我们用Eureka Client来验证下服务的注册。Eureka Client实际上充当Eureka Client角色应该是各种业务的微服务工程了,这里为了快速演示一下服务注册,临时先搞个无意义的client工程作为Eureka Client示范。创建Eureka Client工程eureka-client:File -> New->Product… -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next -> 选择Cloud Discovery -> 选择Eureka Discovery注意每次创建工程后的第一件事,改spring-boot和spring-cloud的版本,不再赘述由于选择了Eureka Discovery,创建成功后pom.xml里已经帮你引入了以下依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>修改pom.xml,还要加入web依赖,不然无法启动成功:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>启动类加注解@EnableDiscoveryClient:package com.hicoview.client;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ClientApplication { public static void main(String[] args) { SpringApplication.run(ClientApplication.class, args); }}配置application.yml:eureka: client: service-url: # 注册中心地址,如果注册中心是高可用,那么这里后面可以添加多个地址,逗号分开 defaultZone: http://localhost:8761/eureka/ #defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/spring: application: name: client启动成后,访问注册中心http://localhost:8761:完成了Eureka服务注册示例,接下来我们简单模拟一个业务场景,示范微服务之间的服务调用。 ...

February 27, 2019 · 1 min · jiezi

「译」Spring Boot 单元测试二三事

本文翻译自:https://reflectoring.io/unit-…原文作者:Tom Hombergs译文原地址:https://weyunx.com/2019/02/04…写好单元测试是一门技术活,不过好在我们现在有很多框架来帮助我们学习。本文就为您介绍这些框架,同时详细介绍编写优秀的 Sping Boot 单元测试所必需的技术细节,我们将了解如何以可测试的方式创建 Spring bean,然后讨论 Mockito 和 AssertJ 的使用,这两个库在默认情况下都集成在 Spring Boot 里。需要注意的是本文只讨论单元测试,组装测试、web 层测试和持久层测试会在后面的文章里讨论。依赖在本文中,我们将使用 JUnit Jupiter (JUnit 5), Mockito, and AssertJ,同时还会引入 Lombok 来省去一些繁复的工作。compileOnly(‘org.projectlombok:lombok’)testCompile(‘org.springframework.boot:spring-boot-starter-test’)testCompile ‘org.junit.jupiter:junit-jupiter-engine:5.2.0’testCompile(‘org.mockito:mockito-junit-jupiter:2.23.0’)spring-boot-starter-test 默认引入了 Mockito and AssertJ,对于 Lombok 则需要我们自己手工引入。不要使用 Spring 进行单元测试看一下下面的「单元」测试,是用来测试 RegisterUseCase 类的一个方法:@ExtendWith(SpringExtension.class)@SpringBootTestclass RegisterUseCaseTest { @Autowired private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { User user = new User(“zaphod”, “zaphod@mail.com”); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); }}我们去执行这个测试类,花了大概 4.5 秒的时间,原因仅仅是因为计算机要为它去运行一个空的 Spring 项目。但是,一个好的单元测试应该是毫秒级的,否则这会影响「test / code / test」的工作方式,这也就是测试驱动开发的思想 (TDD)。即使我们不做 TDD,在编写测试上花了太多时间也会影响我们的开发思路。其实,上面的测试方法实际执行只花费了几毫秒,剩下的 4.5 秒全部花费在了 @SpringBootRun 上,因为 Spring Boot 需要启动整个 Spring Boot 应用。也就是说,我们启动整个应用,耗费了大量资源,仅仅是去为了测试一个方法,当我们的应用未来越来越大的时候,那将耗费更久的时间去启动。所以,为什么不要用 Spring Boot 来做单元测试呢?接下来,本文会讨论如何不用 Spring Boot 来进行单元测试。创建测试类通常,我们可以有如下方法来让我们的 Spring beans 更容易进行测试。不要注入首先我们先看一个错误的例子:@Servicepublic class RegisterUseCase { @Autowired private UserRepository userRepository; public User registerUser(User user) { return userRepository.save(user); }}然而这个类还是必须通过 Spring 才能执行,因为我们无法绕过 UserRepository 这个实例。就像前面提到的,我们必须换一种方法,不使用 @Autowired 来注入 UserRepository。知识点:不要注入写一个构造器我们看一下不使用 @Autowired 的写法:@Servicepublic class RegisterUseCase { private final UserRepository userRepository; public RegisterUseCase(UserRepository userRepository) { this.userRepository = userRepository; } public User registerUser(User user) { return userRepository.save(user); }}这个版本使用构造器来引入 UserRepository 实例。在单元测试中,我们可以像这样来构建一个实例。Spring 会自动的使用构造器来实例化一个 RegisterUseCase 对象。需要注意的是,在 Spring 5 之前,我们需要@Autowired 注解来让构造器生效。同样需要注意的是 UserRepository 字段现在是 final,这样在整个应用的生命周期里,它都将是个常量,这可以避免编码错误,因为我们如果忘记初始化字段,编译的时候就会报错。减少繁复的代码使用 Lombok 的 @RequiredArgsConstructor 注解,可以让构造器的写法更简洁:@Service@RequiredArgsConstructorpublic class RegisterUseCase { private final UserRepository userRepository; public User registerUser(User user) { user.setRegistrationDate(LocalDateTime.now()); return userRepository.save(user); }}现在我们的测试类就很简洁,没有冗余繁复的代码:class RegisterUseCaseTest { private UserRepository userRepository = …; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { User user = new User(“zaphod”, “zaphod@mail.com”); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); }}不过我们还有一点遗漏,就是如何去模拟 UserRepository 实例,因为我们不想去真正的去执行,因为它可能需要去连接数据库。使用 Mockito现行的标准模拟库是 Mockito,它提供了至少两种方式来模拟 UserRepository 。直接调用第一种方法就是直接使用 Mockito:private UserRepository userRepository = Mockito.mock(UserRepository.class);这个创建一个对象,看起来和 UserRepository 一样。默认的情况下,这个类什么也不会做,如果调用有返回值的方法,也只会返回 null。我们的测试现在会是失败,在 assertThat(savedUser.getRegistrationDate()).isNotNull() 这儿报 NullPointerException 空指针异常,因为 userRepository.save(user) 只会返回 null。所以,我们需要告诉 Mockito,当 userRepository.save() 被调用的时候需要有返回值,所以我们使用静态的 when 方法:@Testvoid savedUserHasRegistrationDate() { User user = new User(“zaphod”, “zaphod@mail.com”); when(userRepository.save(any(User.class))).then(returnsFirstArg()); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull();}这样 userRepository.save() 会返回一个对象,其实这个对象和传入参数的对象一摸一样。Mockito 具有一整套的测试方案,可以用来模拟、匹配参数以及识别方法的调用,更多资料可以参考这里。使用 @Mock此外还可以用 @Mock 注解来模拟对象,它需要和 MockitoExtension 组合使用。@ExtendWith(MockitoExtension.class)class RegisterUseCaseTest { @Mock private UserRepository userRepository; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { // … }}@Mock 注解会指定字段将被注入到 mock 对象,@MockitoExtension 会告诉 Mockito 去扫描 @Mock 注解,因为 JUnit 不会自动去执行。这其实和直接手工执行 Mockito.mock() 的结果一样,只是使用习惯的区别。不过使用 MockitoExtension 我们的测试就可以绑定到测试框架里。需要说明的是我们可以在 registerUseCase 字段上使用 @InjectMocks 注解来替代手工构造一个 RegisterUseCase 对象,Mockito 会帮我们自动构造对象,如:@ExtendWith(MockitoExtension.class)class RegisterUseCaseTest { @Mock private UserRepository userRepository; @InjectMocks private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { // … }}让断言更直白另一个 Spring Boot 自带的测试支持库是 AssertJ,上面的例子里,在实现断言的时候已经用到了:assertThat(savedUser.getRegistrationDate()).isNotNull();不过我们想让写法变得更直白好理解,比如:assertThat(savedUser).hasRegistrationDate();通常,我们可以做小改动就可以让代码变得更容易理解,所以我们新建一个自定义的断言对象:public class UserAssert extends AbstractAssert<UserAssert, User> { public UserAssert(User user) { super(user, UserAssert.class); } public static UserAssert assertThat(User actual) { return new UserAssert(actual); } public UserAssert hasRegistrationDate() { isNotNull(); if (actual.getRegistrationDate() == null) { failWithMessage(“Expected user to have a registration date, but it was null”); } return this; }}这样,我们调用 UserAssert 类的 assertThat 方法,而不是直接从 Assertj 库里调用。创建自定义的断言看起来需要很多的工作量,但其实也就是几分钟的事。我相信这几分钟的工作,绝对是值得的,即使是让代码看起来更直白容易理解。测试代码我们只会写一次,然后其他人(包括我在以后)都只是去读这段代码,然后是反反复复的去修改这段代码,直到产品消亡。如果还有疑问,可以参考 Assertions Generator。结论我们可能有种种的理由在 Spring 里进行测试,但是对于一个普通的单元测试,可以这么做,但是没有必要。随着以后应用越来越庞大,启动时间越来越长,可能还会带来问题。所以,我们在写单元测试的时候,应该以一种更简单的方式去构建 Sprnig bean。Spring Boot Test Starter 附带了 Mockito 和 AssertJ 作为测试依赖库,所以尽可能的使用这些测试库来做更好的单元测试吧。所有的代码可以在这里找到。如果发现译文存在错误或其他需要改进的地方,欢迎斧正。 ...

February 27, 2019 · 2 min · jiezi

Spring Security 单点登录简单示例

本文为[原创]文章,转载请标明出处。本文链接:https://weyunx.com/2019/02/12…本文出自微云的技术博客Overview最近在弄单点登录,踩了不少坑,所以记录一下,做了个简单的例子。目标:认证服务器认证后获取 token,客户端访问资源时带上 token 进行安全验证。可以直接看源码。关键依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/></parent><dependencies> <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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.2.RELEASE</version> </dependency></dependencies>认证服务器认证服务器的关键代码有如下几个文件:AuthServerApplication:@SpringBootApplication@EnableResourceServerpublic class AuthServerApplication { public static void main(String[] args) { SpringApplication.run(AuthServerApplication.class, args); }}AuthorizationServerConfiguration 认证配置:@Configuration@EnableAuthorizationServerclass AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Autowired TokenStore tokenStore; @Autowired BCryptPasswordEncoder encoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //配置客户端 clients .inMemory() .withClient(“client”) .secret(encoder.encode(“123456”)).resourceIds(“hi”) .authorizedGrantTypes(“password”,“refresh_token”) .scopes(“read”); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore) .authenticationManager(authenticationManager); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { //允许表单认证 oauthServer .allowFormAuthenticationForClients() .checkTokenAccess(“permitAll()”) .tokenKeyAccess(“permitAll()”); }}代码中配置了一个 client,id 是 client,密码 123456。 authorizedGrantTypes 有 password 和refresh_token 两种方式。SecurityConfiguration 安全配置:@Configuration@EnableWebSecuritypublic class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Bean public TokenStore tokenStore() { return new InMemoryTokenStore(); } @Bean public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .passwordEncoder(encoder()) .withUser(“user_1”).password(encoder().encode(“123456”)).roles(“USER”) .and() .withUser(“user_2”).password(encoder().encode(“123456”)).roles(“ADMIN”); } @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.csrf().disable() .requestMatchers() .antMatchers("/oauth/authorize") .and() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().permitAll(); // @formatter:on } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}上面在内存中创建了两个用户,角色分别是 USER 和 ADMIN。后续可考虑在数据库或者 Redis 中存储相关信息。AuthUser 配置获取用户信息的 Controller:@RestControllerpublic class AuthUser { @GetMapping("/oauth/user") public Principal user(Principal principal) { return principal; }}application.yml 配置,主要就是配置个端口号:—spring: profiles: active: dev application: name: auth-serverserver: port: 8101客户端配置客户端的配置比较简单,主要代码结构如下:application.yml 配置:—spring: profiles: active: dev application: name: clientserver: port: 8102security: oauth2: client: client-id: client client-secret: 123456 access-token-uri: http://localhost:8101/oauth/token user-authorization-uri: http://localhost:8101/oauth/authorize scope: read use-current-uri: false resource: user-info-uri: http://localhost:8101/oauth/user这里主要是配置了认证服务器的相关地址以及客户端的 id 和 密码。user-info-uri 配置的就是服务器端获取用户信息的接口。HelloController 访问的资源,配置了 ADMIN 的角色才可以访问:@RestControllerpublic class HelloController { @RequestMapping("/hi") @PreAuthorize(“hasRole(‘ADMIN’)”) public ResponseEntity<String> hi() { return ResponseEntity.ok().body(“auth success!”); }}WebSecurityConfiguration 相关安全配置:@Configuration@EnableOAuth2Sso@EnableGlobalMethodSecurity(prePostEnabled = true) class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .anyRequest().authenticated(); }}其中 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启后,Spring Security 的 @PreAuthorize,@PostAuthorize 注解才可以使用。@EnableOAuth2Sso 配置了单点登录。ClientApplication:@SpringBootApplication@EnableResourceServerpublic class ClientApplication { public static void main(String[] args) { SpringApplication.run(ClientApplication.class, args); }}验证启动项目后,我们使用 postman 来进行验证。首先是获取 token:选择 POST 提交,地址为验证服务器的地址,参数中输入 username,password,grant_type 和 scope ,其中 grant_type 需要输入 password。然后在下面等 Authorization 标签页中,选择 Basic Auth,然后输入 client 的 id 和 password。{ “access_token”: “02f501a9-c482-46d4-a455-bf79a0e0e728”, “token_type”: “bearer”, “refresh_token”: “0e62dddc-4f51-4cb5-81c3-5383fddbb81b”, “expires_in”: 41741, “scope”: “read”}此时就可以获得 access_token 为: 02f501a9-c482-46d4-a455-bf79a0e0e728。需要注意的是这里是用 user_2 获取的 token,即角色是 ADMIN。然后我们再进行获取资源的验证:使用 GET 方法,参数中输入 access_token,值输入 02f501a9-c482-46d4-a455-bf79a0e0e728 。点击提交后即可获取到结果。如果我们不加上 token ,则会提示无权限。同样如果我们换上 user_1 获取的 token,因 user_1 的角色是 USER,此资源需要 ADMIN 权限,则此处还是会获取失败。简单的例子就到这,后续有时间再加上其它功能吧,谢谢~未完待续… ...

February 27, 2019 · 2 min · jiezi

Spring Boot 之 LogBack 配置

本文为[原创]文章,转载请标明出处。原文链接:https://weyunx.com/2019/02/01…原文出自微云的技术博客LogBack 默认集成在 Spring Boot 中,是基于 Slf4j 的日志框架。默认情况下 Spring Boot 是以 INFO 级别输出到控制台。它的日志级别是:ALL < TRACE < DEBUG < INFO < WARN < ERROR < OFF配置LogBack 可以直接在 application.properties 或 application.yml 中配置,但仅支持一些简单的配置,复杂的文件输出还是需要配置在 xml 配置文件中。配置文件可命名为 logback.xml , LogBack 自动会在 classpath 的根目录下搜索配置文件,不过 Spring Boot 建议命名为 logback-spring.xml,这样会自动引入 Spring Boot 一些扩展功能。如果需要引入自定义名称的配置文件,需要在 Spring Boot 的配置文件中指定,如:logging: config: classpath:logback-spring.xml同时 Spring Boot 提供了一个默认的 base.xml 配置,可以按照如下方式引入:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/base.xml”/></configuration>base.xml 提供了一些基本的默认配置以及在控制台输出时的关键字配色,具体文件内容可以看这里,可以查看到一些常用的配置写法。详细配置变量可以使用 <property> 来定义变量:<property name=“log.path” value="/var/logs/application" />同时可以引入 Spring 的环境变量:<property resource=“application.yml” /><property resource=“application.properties” />所有的变量都可以通过 ${} 来调用。输出到控制台<?xml version=“1.0” encoding=“UTF-8”?><configuration> <appender name=“console” class=“ch.qos.logback.core.ConsoleAppender”> <encoder> <pattern>%.-1level|%-40.40logger{0}|%msg%n</pattern> </encoder> </appender> <logger name=“com.mycompany.myapp” level=“debug” /> <logger name=“org.springframework” level=“info” /> <logger name=“org.springframework.beans” level=“debug” /> <root level=“warn”> <appender-ref ref=“console” /> </root></configuration>输出到文件<property name=“LOG_FILE” value=“LogFile” /><appender name=“FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <file>${LOG_FILE}.log</file> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <!– 每日归档日志文件 –> <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern> <!– 保留 30 天的归档日志文件 –> <maxHistory>30</maxHistory> <!– 日志文件上限 3G,超过后会删除旧的归档日志文件 –> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder></appender> 多环境配置LogBack 同样支持多环境配置,如 dev 、 test 、 prod<springProfile name=“dev”> <logger name=“com.mycompany.myapp” level=“debug”/></springProfile>启动的时候 java -jar xxx.jar –spring.profiles.active=dev 即可使配置生效。如果要使用 Spring 扩展的 profile 支持,配置文件名必须命名为 LogBack_Spring.xml,此时当 application.properties 中指定为 spring.profiles.active=dev 时,上述配置才会生效。参考https://docs.spring.io/spring…https://dzone.com/articles/en...https://dzone.com/articles/co...http://tengj.top/2017/04/05/s… ...

February 27, 2019 · 1 min · jiezi

Spring Boot 最核心的 25 个注解,都是干货!

学习和应用 Spring Boot 有一些时间了,你们对 Spring Boot 注解了解有多少呢?今天栈长我给大家整理了 Spring Boot 最核心的 25 个注解,都是干货!你所需具备的基础什么是 Spring Boot?Spring Boot 核心配置文件详解Spring Boot 开启的 2 种方式Spring Boot 自动配置原理、实战Spring Boot 2.x 启动全过程源码分析Java 必须掌握的 12 种 Spring 常用注解!更多请在Java技术栈微信公众号后台回复关键字:boot。Spring Boot 最核心的 25 个注解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 配置文件,所有这个配置文件里面能做到的事情都可以通过这个注解所在类来进行注册。4、@SpringBootConfiguration这个注解就是 @Configuration 注解的变体,只是用来修饰是 Spring Boot 配置而已,或者可利于 Spring Boot 后续的扩展。5、@ComponentScan这是 Spring 3.1 添加的一个注解,用来代替配置文件中的 component-scan 配置,开启组件扫描,即自动扫描包路径下的 @Component 注解进行注册 bean 实例到 context 中。前面 5 个注解可以在这篇文章《Spring Boot 最核心的 3 个注解详解》中了解更多细节的。6、@Conditional这是 Spring 4.0 添加的新注解,用来标识一个 Spring Bean 或者 Configuration 配置文件,当满足指定的条件才开启配置。7、@ConditionalOnBean组合 @Conditional 注解,当容器中有指定的 Bean 才开启配置。8、@ConditionalOnMissingBean组合 @Conditional 注解,和 @ConditionalOnBean 注解相反,当容器中没有指定的 Bean 才开启配置。9、@ConditionalOnClass组合 @Conditional 注解,当容器中有指定的 Class 才开启配置。10、@ConditionalOnMissingClass组合 @Conditional 注解,和 @ConditionalOnMissingClass 注解相反,当容器中没有指定的 Class 才开启配置。11、@ConditionalOnWebApplication组合 @Conditional 注解,当前项目类型是 WEB 项目才开启配置。当前项目有以下 3 种类型。enum Type { /** * Any web application will match. / ANY, /* * Only servlet-based web application will match. / SERVLET, /* * Only reactive-based web application will match. */ REACTIVE}12、@ConditionalOnNotWebApplication组合 @Conditional 注解,和 @ConditionalOnWebApplication 注解相反,当前项目类型不是 WEB 项目才开启配置。13、@ConditionalOnProperty组合 @Conditional 注解,当指定的属性有指定的值时才开启配置。14、@ConditionalOnExpression组合 @Conditional 注解,当 SpEL 表达式为 true 时才开启配置。15、@ConditionalOnJava组合 @Conditional 注解,当运行的 Java JVM 在指定的版本范围时才开启配置。16、@ConditionalOnResource组合 @Conditional 注解,当类路径下有指定的资源才开启配置。17、@ConditionalOnJndi组合 @Conditional 注解,当指定的 JNDI 存在时才开启配置。18、@ConditionalOnCloudPlatform组合 @Conditional 注解,当指定的云平台激活时才开启配置。19、@ConditionalOnSingleCandidate组合 @Conditional 注解,当指定的 class 在容器中只有一个 Bean,或者同时有多个但为首选时才开启配置。20、@ConfigurationProperties用来加载额外的配置(如 .properties 文件),可用在 @Configuration 注解类,或者 @Bean 注解方法上面。关于这个注解的用法可以参考《Spring Boot读取配置的几种方式》这篇文章。21、@EnableConfigurationProperties一般要配合 @ConfigurationProperties 注解使用,用来开启对 @ConfigurationProperties 注解配置 Bean 的支持。22、@AutoConfigureAfter用在自动配置类上面,表示该自动配置类需要在另外指定的自动配置类配置完之后。如 Mybatis 的自动配置类,需要在数据源自动配置类之后。@AutoConfigureAfter(DataSourceAutoConfiguration.class)public class MybatisAutoConfiguration {23、@AutoConfigureBefore这个和 @AutoConfigureAfter 注解使用相反,表示该自动配置类需要在另外指定的自动配置类配置之前。24、@Import这是 Spring 3.0 添加的新注解,用来导入一个或者多个 @Configuration 注解修饰的类,这在 Spring Boot 里面应用很多。25、@ImportResource这是 Spring 3.0 添加的新注解,用来导入一个或者多个 Spring 配置文件,这对 Spring Boot 兼容老项目非常有用,因为有些配置无法通过 Java Config 的形式来配置就只能用这个注解来导入。好了,终于总结完了,花了栈长我 2 个小时。。。另外,关注微信公众号Java技术栈,在后台回复关键字:boot, 可以获取栈长整理的更多 Spring Boot 系列教程文章。本文原创首发于微信公众号:Java技术栈(id:javastack),关注公众号在后台回复 “boot” 可获取更多,转载请原样保留本信息。 ...

February 26, 2019 · 2 min · jiezi

SpringBoot 实战 (十二) | 整合 thymeleaf

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 Thymeleaf ,并整合 Thymeleaf 开发一个简陋版的学生信息管理系统。SpringBoot 提供了大量模板引擎,包含 Freemarker、Groovy、Thymeleaf、Velocity 以及 Mustache,SpringBoot 中推荐使用 Thymeleaf 作为模板引擎,因为 Thymeleaf 提供了完美的 SpringMVC 支持。Thymeleaf 是新一代 Java 模板引擎,在 Spring 4 后推荐使用。什么是模板引擎?Thymeleaf 是一种模板语言。那模板语言或模板引擎是什么?常见的模板语言都包含以下几个概念:数据(Data)、模板(Template)、模板引擎(Template Engine)和结果文档(Result Documents)。数据数据是信息的表现形式和载体,可以是符号、文字、数字、语音、图像、视频等。数据和信息是不可分离的,数据是信息的表达,信息是数据的内涵。数据本身没有意义,数据只有对实体行为产生影响时才成为信息。模板模板,是一个蓝图,即一个与类型无关的类。编译器在使用模板时,会根据模板实参对模板进行实例化,得到一个与类型相关的类。模板引擎模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。结果文档一种特定格式的文档,比如用于网站的模板引擎就会生成一个标准的HTML文档。模板语言用途广泛,常见的用途如下:页面渲染文档生成代码生成所有 “数据+模板=文本” 的应用场景Thymeleaf 简介Thymeleaf 是一个 Java 类库,它是一个 xml/xhtml/html5 的模板引擎,可以作为 MVC 的 web 应用的 View 层。Thymeleaf 还提供了额外的模块与 SpringMVC 集成,所以我们可以使用 Thymeleaf 完全替代 JSP 。Thymeleaf 语法博客资料:http://www.cnblogs.com/nuoyia…官方文档:http://www.thymeleaf.org/docu…SpringBoot 整合 Thymeleaf下面使用 SpringBoot 整合 Thymeleaf 开发一个简陋版的学生信息管理系统。1、准备工作IDEAJDK1.8SpringBoot2.1.32、pom.xml 主要依赖<dependencies> <!– JPA 数据访问 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!– thymeleaf 模板引擎 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!– web 启动类 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mysql 数据库连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency></dependencies>3、application.yaml 文件配置spring: # 数据库相关 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: root password: 123456 # jpa 相关 jpa: hibernate: ddl-auto: update # ddl-auto: 第一次启动项目设为 create 表示每次都重新建表,之后设置为 update show-sql: true4、实体类@Data@Entity@AllArgsConstructor@NoArgsConstructorpublic class Student { @Id @GeneratedValue /** * 主键 / private Long id; /* * 主键 / private Long studentId; /* * 姓名 / private String name; /* * 年龄 / private Integer age; /* * 专业 / private String major; /* * 宿舍 / private String dormitory; /* * 籍贯 / private String city; /@Temporal(TemporalType.TIMESTAMP)//将时间戳,转换成年月日时分秒的日期格式 @Column(name = “create_time”,insertable = false, updatable=false, columnDefinition = “timestamp default current_timestamp comment ‘注册时间’”) private Date createDate; @Temporal(TemporalType.TIMESTAMP)//将时间戳,转换成年月日时分秒的日期格式 @Column(name = “update_time”,insertable = false, updatable=true, columnDefinition = “timestamp default current_timestamp comment ‘修改时间’”) private Date updateDate;/}5、dao 层@Repositorypublic interface StudentRepository extends JpaRepository<Student, Long> {}6、service 层public interface StudentService { List<Student> findStudentList(); Student findStudentById(Long id); Student saveStudent(Student student); Student updateStudent(Student student); void deleteStudentById(Long id);}实现类:@Servicepublic class StudentServiceImpl implements StudentService { @Autowired private StudentRepository studentRepository; /** * 查询所有学生信息列表 * @return / @Override public List<Student> findStudentList() { Sort sort = new Sort(Direction.ASC,“id”); return studentRepository.findAll(sort); } /* * 根据 id 查询单个学生信息 * @param id * @return / @Override public Student findStudentById(Long id) { return studentRepository.findById(id).get(); } /* * 保存学生信息 * @param student * @return / @Override public Student saveStudent(Student student) { return studentRepository.save(student); } /* * 更新学生信息 * @param student * @return / @Override public Student updateStudent(Student student) { return studentRepository.save(student); } /* * 根据 id 删除学生信息 * @param id * @return / @Override public void deleteStudentById(Long id) { studentRepository.deleteById(id); }}7、controller 层 (Thymeleaf) 使用controller 层将 view 指向 Thymeleaf:@Controller@RequestMapping("/student")public class StudentController { @Autowired private StudentService studentService; /* * 获取学生信息列表 * @param map * @return / @GetMapping("/list") public String findStudentList(ModelMap map) { map.addAttribute(“studentList”,studentService.findStudentList()); return “studentList”; } /* * 获取保存 student 表单 / @GetMapping(value = “/create”) public String createStudentForm(ModelMap map) { map.addAttribute(“student”, new Student()); map.addAttribute(“action”, “create”); return “studentForm”; } /* * 保存学生信息 * @param student * @return / @PostMapping(value = “/create”) public String saveStudent(@ModelAttribute Student student) { studentService.saveStudent(student); return “redirect:/student/list”; } /* * 根据 id 获取 student 表单,编辑后提交更新 * @param id * @param map * @return / @GetMapping(value = “/update/{id}”) public String edit(@PathVariable Long id, ModelMap map) { map.addAttribute(“student”, studentService.findStudentById(id)); map.addAttribute(“action”, “update”); return “studentForm”; } /* * 更新学生信息 * @param student * @return / @PostMapping(value = “/update”) public String updateStudent(@ModelAttribute Student student) { studentService.updateStudent(student); return “redirect:/student/list”; } /* * 删除学生信息 * @param id * @return / @GetMapping(value = “/delete/{id}”) public String deleteStudentById(@PathVariable Long id) { studentService.deleteStudentById(id); return “redirect:/student/list”; }}简单说下,ModelMap 对象来进行数据绑定到视图。return 字符串,该字符串对应的目录在 resources/templates 下的模板名字。 @ModelAttribute 注解是用来获取页面 Form 表单提交的数据,并绑定到 Student 数据对象。8、studentForm 表单定义了一个 Form 表单用于注册或修改学生信息。<form th:action="@{/student/{action}(action=${action})}" method=“post” class=“form-horizontal”> <div class=“form-group”> <label for=“student_Id” class=“col-sm-2 control-label”>学号:</label> <div class=“col-xs-4”> <input type=“text” class=“form-control” id=“student_Id” name=“name” th:value="${student.studentId}" th:field="{student.studentId}"/> </div> </div> <div class=“form-group”> <label for=“student_name” class=“col-sm-2 control-label”>姓名:</label> <div class=“col-xs-4”> <input type=“text” class=“form-control” id=“student_name” name=“name” th:value="${student.name}" th:field="{student.name}"/> </div> </div> <div class=“form-group”> <label for=“student_age” class=“col-sm-2 control-label”>年龄:</label> <div class=“col-xs-4”> <input type=“text” class=“form-control” id=“student_age” name=“name” th:value="${student.age}" th:field="{student.age}"/> </div> </div> <div class=“form-group”> <label for=“student_major” class=“col-sm-2 control-label”>专业:</label> <div class=“col-xs-4”> <input type=“text” class=“form-control” id=“student_major” name=“name” th:value="${student.major}" th:field="{student.major}"/> </div> </div> <div class=“form-group”> <label for=“student_dormitory” class=“col-sm-2 control-label”>宿舍:</label> <div class=“col-xs-4”> <input type=“text” class=“form-control” id=“student_dormitory” name=“name” th:value="${student.dormitory}" th:field="{student.dormitory}"/> </div> </div> <div class=“form-group”> <label for=“student_city” class=“col-sm-2 control-label”>籍贯:</label> <div class=“col-xs-4”> <input type=“text” class=“form-control” id=“student_city” name=“writer” th:value="${student.city}" th:field="{student.city}"/> </div> </div> <div class=“form-group”> <div class=“col-sm-offset-3 col-sm-10”> <input class=“btn btn-primary” type=“submit” value=“提交”/>&nbsp;&nbsp; <input class=“btn” type=“button” value=“返回” onclick=“history.back()”/> </div> </div> </form>9、studentList 学生列表用于展示学生信息:<table class=“table table-hover table-condensed”> <legend> <strong>学生信息列表</strong> </legend> <thead> <tr> <th>学号</th> <th>姓名</th> <th>年龄</th> <th>专业</th> <th>宿舍</th> <th>籍贯</th> <th>管理</th> </tr> </thead> <tbody> <tr th:each=“student : ${studentList}"> <th scope=“row” th:text="${student.studentId}"></th> <td><a th:href=”@{/student/update/{studentId}(studentId=${student.id})}" th:text="${student.name}"></a></td> <td th:text="${student.age}"></td> <td th:text="${student.major}"></td> <td th:text="${student.dormitory}"></td> <td th:text="${student.city}"></td> <td><a class=“btn btn-danger” th:href="@{/student/delete/{studentId}(studentId=${student.id})}">删除</a></td> </tr> </tbody> </table>页面效果列表页面:点击按钮可注册学生信息注册/修改学生信息页面:点提交保存学生信息到数据库并返回列表页面有数据的列表页面:点击名字跳到注册/修改页面可修改学生信息,点击删除可删除学生信息后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 25, 2019 · 4 min · jiezi

SpringBoot 填坑 | CentOS7.4 环境下,MySQL5.7 表时间字段默认值设置失效

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天这篇是一个刚认识不久的小师弟的投稿。交谈中感觉技术水平与代码素养非常高,关键是才大二呀。那会我应该还在玩泥巴吧,真是后生可畏。问题描述我在本地端( windos 端,数据库版本 MySQL5.7、SpringBoot2.1.3、数据访问框架 JPA)测试代码时 current_timestamp 属性只要设有置默认值,就会自动生成数据的创建时间,与修改数据之后的修改时间。但是在 CentOS 服务器中。调用 JPA 中 save() 方法。字段却不会自动生成了。1、这是其中一张数据库的案例:CREATE TABLE user_info ( id int(32) NOT NULL AUTO_INCREMENT, username varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, password varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, shop_type int(11) NULL DEFAULT NULL COMMENT ‘店铺编号’, salt varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘盐’, status int(64) NOT NULL COMMENT ‘账号状态’, openid varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘微信openid’, create_time timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT ‘创建时间’, update_time timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT ‘修改时间’, PRIMARY KEY (id) USING BTREE, UNIQUE INDEX upe_seller_info_username(username) USING BTREE);从上面 SQL 示例可以注意到表字段,创建时间和更新时间设置了默认值 CURRENT_TIMESTAMP(0) 。2、这是发送的创建用户请求,里面的逻辑有 save 方法:3、这是在线上服务器报的错误问题排查前面我说了,我已经设置了字段有默认值的。。但是为什么在线上服务器居然没有自动生成。我百思不得其解,在本地端安然无恙,怎么线上环境炸了呢?而且我还在日志中发现一般都是 insert 中会出错误。尝试解决:首先我在 entity 层中删除了createtime,updatetime,果然不报空了。但是在我的 freemarker 上又必须有这个字段怎么办呢?解决问题在你的 createtime,updatetime 上分别加上 @CreatedDate 和 @LastModifiedDate 在 entity 类上加注解 @EntityListeners(AuditingEntityListener.class) 还要在你的启动类加上 @EnableJpaAuditing ,问题迎刃而解。entity类@Data@Entity@DynamicUpdate // 生成动态SQL语句,即在插入和修改数据的时候,语句中只包括要插入或者修改的字段。@EntityListeners(AuditingEntityListener.class)public class UserInfo { @Id @GeneratedValue private Integer id; private String username; private String password; //店铺编号 private Integer shopType; //加盐 private String salt; private Integer status; //卖家微信openid private String openid; //创建时间 @CreatedDate @JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”) private Date createTime; //更新时间 @LastModifiedDate @JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”) private Date updateTime;}启动类@SpringBootApplication@EnableSwagger2@EnableJpaAuditingpublic class ShipApplication { public static void main(String[] args) { SpringApplication.run(ShipApplication.class, args); }}至此,问题解决。注解解释@CreatedDate //表示该字段为创建时间时间字段,在这个实体被insert的时候,会设置值@LastModifiedDate //同理@EntityListeners(AuditingEntityListener.class) // JPA审计@EnableJpaAuditing//开启JPA审计我的思考我个人的理解是当我们添加这些注解后,JPA 的审计功能会把值再重复设置进 createtime,updatetime 这两个字段里面,第一遍是数据库层默认值,第二遍就是代码层设置的。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 24, 2019 · 1 min · jiezi

SpringBoot 实战 (十一) | 整合数据缓存 Cache

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 SpringBoot 的数据缓存。做过开发的都知道程序的瓶颈在于数据库,我们也知道内存的速度是大大快于硬盘的,当需要重复获取相同数据时,一次又一次的请求数据库或者远程服务,导致大量时间耗费在数据库查询或远程方法调用上,导致性能的恶化,这便是数据缓存要解决的问题。Spring 的缓存支持Spring 定义了 org.springframework.cache.CacheManager 和 org.springframework.cache.Cache 接口用于统一不同的缓存技术。其中,CacheManager 是 Spring 提供的各种缓存技术的抽象接口,Cache 接口则是包含了缓存的各种操作(增加,删除,获取缓存,一般不会直接和此接口打交道)。1、Spring 支持的 CacheManager 针对不同的缓存技术,实现了不同的 CacheManager ,Spring 定义了下表所示的 CacheManager:CacheManager描述SimpleCacheManager使用简单的 Collection 来存储缓存,主要用于测试ConcurrentMapCacheManager使用 ConcurrentMap 来存储缓存NoOpCacheManager仅测试用途,不会实际缓存数据EhCacheCacheManager使用 EhCache 作为缓存技术GuavaCacheManager使用 Google Guava 的 GuavaCache 作为缓存技术HazelcastCacheManager使用 Hazelcast 作为缓存技术JCacheCacheManager支持 JCache(JSR-107) 标准的实现作为缓存技术,如 ApacheCommonsJCSRedisCacheManager使用 Redis 作为缓存技术在使用以上任意一个实现的 CacheManager 的时候,需注册实现的 CacheManager 的 Bean,如:@Beanpublic EhCacheCacheManager cacheManager(CacheManager ehCacheCacheManager){ return new EhCacheCacheManager(ehCacheCacheManager);}注意,每种缓存技术都有很多的额外配置,但配置 cacheManager 是必不可少的。2、声明式缓存注解Spring 提供了 4 个注解来声明缓存规则(又是使用注解式的 AOP 的一个例子)。4 个注解如下表示:注解解释@Cacheable在方法执行前 Spring 先查看缓存中是否有数据,若有,则直接返回缓存数据;若无数据,调用方法将方法返回值放入缓存中@CachePut无论怎样,都会将方法的返回值放到缓存中。@CacheEvict将一条或多条数据从缓存中删除@Caching可以通过 @Caching 注解组合多个注解策略在一个方法上@Cacheable、@CachePut、@CacheEvict 都有 value 属性,指定的是要使用的缓存名称;key 属性指定的是数据在缓存中存储的键。3、开启声明式缓存支持开启声明式缓存很简单,只需在配置类上使用 @EnableCaching 注解即可,例如:@Configuration@EnableCachingpublic class AppConfig{}SpringBoot 的支持在 Spring 中使用缓存技术的关键是配置 CacheManager ,而 SpringBoot 为我们配置了多个 CacheManager 的实现。它的自动配置放在 org.springframework.boot.autoconfigure.cache 包中。在不做任何配置的情况下,默认使用的是 SimpleCacheConfiguration ,即使用 ConcurrentMapCacheManager。SpringBoot 支持以前缀来配置缓存。例如:spring.cache.type= # 可选 generic、ehcache、hazelcast、infinispan、jcache、redis、guava、simple、nonespring.cache.cache-names= # 程序启动时创建的缓存名称spring.cache.ehcache.config= # ehcache 配置文件的地址spring.cache.hazelcast.config= # hazelcast配置文件的地址spring.cache.infinispan.config= # infinispan配置文件的地址spring.cache.jcache.config= # jcache配置文件的地址spring.cache.jcache.provider= # 当多个 jcache 实现在类路径的时候,指定 jcache 实现# 等等。。。在 SpringBoot 环境下,使用缓存技术只需要在项目中导入相关缓存技术的依赖包,并在配置类中使用 @EnableCaching 开启缓存支持即可。代码实现本文将以 SpringBoot 默认的 ConcurrentMapCacheManager 作为缓存技术,演示 @Cacheable、@CachePut、@CacheEvict。1、准备工作IDEAJDK 1.8SpringBoot 2.1.32、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.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>cache</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cache</name> <description>cache Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!– cache 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!– JPA 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!– web 启动类 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mysql 数据库连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– lombok 依赖,简化实体 –> <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>注释很清楚,无需多言。不会就谷歌一下。3、Application.yaml 文件配置spring: # 数据库相关 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: root password: 123456 # jpa 相关 jpa: hibernate: ddl-auto: update # ddl-auto: 设为 create 表示每次都重新建表 show-sql: true4、实体类package com.nasus.cache.entity;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@Entity@AllArgsConstructor@NoArgsConstructorpublic class Student { @Id @GeneratedValue private Integer id; private String name; private Integer age;}5、dao 层package com.nasus.cache.repository;import com.nasus.cache.entity.Student;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;@Repositorypublic interface StudentRepository extends JpaRepository<Student,Integer> {}6、service 层package com.nasus.cache.service;import com.nasus.cache.entity.Student;public interface StudentService { public Student saveStudent(Student student); public void deleteStudentById(Integer id); public Student findStudentById(Integer id);}实现类:package com.nasus.cache.service.impl;import com.nasus.cache.entity.Student;import com.nasus.cache.repository.StudentRepository;import com.nasus.cache.service.StudentService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service;@Servicepublic class StudentServiceImpl implements StudentService { // 使用 slf4j 作为日志框架 private static final Logger LOGGER = LoggerFactory.getLogger(StudentServiceImpl.class); @Autowired private StudentRepository studentRepository; @Override @CachePut(value = “student”,key = “#student.id”) // @CachePut 缓存新增的或更新的数据到缓存,其中缓存名称为 student 数据的 key 是 student 的 id public Student saveStudent(Student student) { Student s = studentRepository.save(student); LOGGER.info(“为id、key 为{}的数据做了缓存”, s.getId()); return s; } @Override @CacheEvict(value = “student”) // @CacheEvict 从缓存 student 中删除 key 为 id 的数据 public void deleteStudentById(Integer id) { LOGGER.info(“删除了id、key 为{}的数据缓存”, id); //studentRepository.deleteById(id); } @Override @Cacheable(value = “student”,key = “#id”) // @Cacheable 缓存 key 为 id 的数据到缓存 student 中 public Student findStudentById(Integer id) { Student s = studentRepository.findById(id).get(); LOGGER.info(“为id、key 为{}的数据做了缓存”, id); return s; }}代码讲解看注释,很详细。7、controller 层package com.nasus.cache.controller;import com.nasus.cache.entity.Student;import com.nasus.cache.service.StudentService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.Mapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/student”)public class StudentController { @Autowired private StudentService studentService; @PostMapping("/put”) public Student saveStudent(@RequestBody Student student){ return studentService.saveStudent(student); } @DeleteMapping("/evit/{id}”) public void deleteStudentById(@PathVariable(“id”) Integer id){ studentService.deleteStudentById(id); } @GetMapping("/able/{id}") public Student findStudentById(@PathVariable(“id”) Integer id){ return studentService.findStudentById(id); }}8、application 开启缓存功能package com.nasus.cache;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cache.annotation.EnableCaching;@EnableCaching // 开启缓存功能@SpringBootApplicationpublic class CacheApplication { public static void main(String[] args) { SpringApplication.run(CacheApplication.class, args); }}测试测试前,先看一眼数据库当前的数据,如下:1、测试 @Cacheable 访问 http://localhost:8080/student/able/2 控制台打印出了 SQL 查询语句,以及指定日志。说明这一次程序是直接查询数据库得到的结果。2019-02-21 22:54:54.651 INFO 1564 — [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 11 msHibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?2019-02-21 22:54:59.725 INFO 1564 — [nio-8080-exec-1] c.n.c.service.impl.StudentServiceImpl : 为id、key 为2的数据做了缓存postman 第一次测试结果 :再次访问 http://localhost:8080/student/able/2 结果如下图。但控制台无 SQL 语句打印,也无为id、key 为2的数据做了缓存这句话输出。说明 @Cacheable 确实做了数据缓存,第二次的测试结果是从数据缓存中获取的,并没有直接查数据库。2、测试 @CachePut如下图,postman 访问 http://localhost:8080/student/put 插入数据:下面是控制台打印出了 SQL Insert 插入语句,以及指定日志。说明程序做了缓存。Hibernate: insert into student (age, name, id) values (?, ?, ?)2019-02-21 23:12:03.688 INFO 1564 — [nio-8080-exec-8] c.n.c.service.impl.StudentServiceImpl : 为id、key 为4的数据做了缓存插入数据返回的结果:数据库中的结果:访问 http://localhost:8080/student/able/4 Postman 结果如下图。控制台无输出,验证了 @CachePut 确实做了缓存,下图数据是从缓存中获取的。3、测试 @CacheEvict postman 访问 http://localhost:8080/student/able/3 为 id = 3 的数据做缓存。postman 再次访问 http://localhost:8080/student/able/3 确认数据是从缓存中获取的。postman 访问 http://localhost:8080/student/evit/3 从缓存中删除 key 为 3 的缓存数据:Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?2019-02-21 23:26:08.516 INFO 8612 — [nio-8080-exec-2] c.n.c.service.impl.StudentServiceImpl : 为id、key 为3的数据做了缓存2019-02-21 23:27:01.508 INFO 8612 — [nio-8080-exec-4] c.n.c.service.impl.StudentServiceImpl : 删除了id、key 为3的数据缓存再次 postman 访问 http://localhost:8080/student/able/3 观察后台,重新做了数据缓存:Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?2019-02-21 23:27:12.320 INFO 8612 — [nio-8080-exec-5] c.n.c.service.impl.StudentServiceImpl : 为id、key 为3的数据做了缓存这一套测试流程下来,证明了 @CacheEvict 确实删除了数据缓存。源码下载https://github.com/turoDog/Demo/tree/master/springboot_cache_demo切换缓存技术切换缓存技术除了在 pom 文件加入相关依赖包配置以外,使用方式与上面的代码演示一样。1、切换 EhCache 在 pom 中添加 Encache 依赖:<!– EhCache 依赖 –><dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId></dependency>Ehcache 所需配置文件 ehcache.xml 只需放在类路径(resource 目录)下,SpringBoot 会自动扫描,如:<?xml version=“1.0” encoding=“UTF-8”><ehcache> <cache name=“student” maxElementsInMmory=“1000”><ehcache>SpringBoot 会自动配置 EhcacheManager 的 Bean。2、切换 Guava只需在 pom 中加入 Guava 依赖即可:<!– GuavaCache 依赖 –><dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version></dependency>SpringBoot 会自动配置 GuavaCacheManager 的 Bean。3、切换 RedisCache与 Guava 一样,只需在 pom 加入依赖即可:<!– cache 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId></dependency>SpringBoot 会自动配置 RedisCacheManager 以及 RedisTemplate 的 Bean。此外,切换其他缓存技术的方式也是类似。这里不做赘述。后语以上为 SpringBoot 数据缓存的教程。如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 23, 2019 · 4 min · jiezi

领域驱动设计,构建简单的新闻系统,20分钟够吗?

让我们使用领域驱动的方式,构建一个简单的系统。1. 需求新闻系统的需求如下:创建新闻类别;修改新闻类别,只能更改名称;禁用新闻类别,禁用后的类别不能添加新闻;启用新闻类别;根据类别id获取类别信息;指定新闻类别id,创建新闻;更改新闻信息,只能更改标题和内容;禁用新闻;启用新闻;分页查找给定类别的新闻,禁用的新闻不可见。2. 工期估算大家觉得,针对上面需求,大概需要多长时间可以完成,可以先写下来。3. 起航3.1. 项目准备构建项目,使用 http://start.spring.io 或使用模板工程,构建我们的项目(Sprin Boot 项目),在这就不多叙述。3.1.1. 添加依赖首先,添加 gh-ddd-lite 相关依赖和插件。<?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> <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> <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <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>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <executable>true</executable> <layout>ZIP</layout> </configuration> </plugin> <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> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> <!–<processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor>–> </configuration> </execution> </executions> </plugin> </plugins> </build></project>3.1.2. 添加配置信息在 application.properties 文件中添加数据库相关配置。spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=spring.application.name=ddd-lite-demoserver.port=8090management.endpoint.beans.enabled=truemanagement.endpoint.conditions.enabled=truemanagement.endpoints.enabled-by-default=falsemanagement.endpoints.web.exposure.include=beans,conditions,env3.1.3. 添加入口类新建 UserApplication 作为应用入口类。@SpringBootApplication@EnableSwagger2public class UserApplication { public static void main(String… args){ SpringApplication.run(UserApplication.class, args); }}使用 SpringBootApplication 和 EnableSwagger2 启用 Spring Boot 和 Swagger 特性。3.2. NewsCategory 建模首先,我们对新闻类型进行建模。3.2.1. 建模 NewsCategory 状态新闻类别状态,用于描述启用、禁用两个状态。在这使用 enum 实现。/** * GenCodeBasedEnumConverter 自动生成 CodeBasedNewsCategoryStatusConverter 类 /@GenCodeBasedEnumConverterpublic enum NewsCategoryStatus implements CodeBasedEnum<NewsCategoryStatus> { ENABLE(1), DISABLE(0); private final int code; NewsCategoryStatus(int code) { this.code = code; } @Override public int getCode() { return code; }}3.2.2. 建模 NewsCategoryNewsCategory 用于描述新闻类别,其中包括状态、名称等。3.2.2.1. 新建 NewsCategory/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@EnableGenForAggregate@Data@Entity@Table(name = “tb_news_category”)public class NewsCategory extends JpaAggregate { private String name; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsCategoryStatusConverter.class) private NewsCategoryStatus status;}3.2.2.2. 自动生成 Base 代码在命令行或ida中执行maven命令,以对项目进行编译,从而触发代码的自动生成。mvn clean compile3.2.2.3. 建模 NewsCategory 创建逻辑我们使用 NewsCategory 的静态工厂,完成其创建逻辑。首先,需要创建 NewsCategoryCreator,作为工程参数。public class NewsCategoryCreator extends BaseNewsCategoryCreator<NewsCategoryCreator>{}其中 BaseNewsCategoryCreator 为框架自动生成的,具体如下:@Datapublic abstract class BaseNewsCategoryCreator<T extends BaseNewsCategoryCreator> { @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; public void accept(NewsCategory target) { target.setName(getName()); }}接下来,需要创建静态工程,并完成 NewsCategory 的初始化。/* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return /public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category;}/* * 初始化,默认状态位 ENABLE /private void init() { setStatus(NewsCategoryStatus.ENABLE);}3.2.2.4. 建模 NewsCategory 更新逻辑更新逻辑,只对 name 进行更新操作。首先,创建 NewsCategoryUpdater 作为,更新方法的参数。public class NewsCategoryUpdater extends BaseNewsCategoryUpdater<NewsCategoryUpdater>{}同样,BaseNewsCategoryUpdater 也是框架自动生成,具体如下:@Datapublic abstract class BaseNewsCategoryUpdater<T extends BaseNewsCategoryUpdater> { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private DataOptional<String> name; 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(NewsCategory target) { this.acceptName(target::setName); }}添加 update 方法:/* * 更新 * @param updater /public void update(NewsCategoryUpdater updater){ updater.accept(this);} 3.2.2.5. 建模 NewsCategory 启用逻辑启用,主要是对 status 的操作.代码如下:/* * 启用 /public void enable(){ setStatus(NewsCategoryStatus.ENABLE);}3.2.2.6. 建模 NewsCategory 禁用逻辑禁用,主要是对 status 的操作。代码如下:/* * 禁用 /public void disable(){ setStatus(NewsCategoryStatus.DISABLE);}至此,NewsCategory 的 Command 就建模完成,让我们总体看下 NewsCategory:/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@EnableGenForAggregate@Data@Entity@Table(name = “tb_news_category”)public class NewsCategory extends JpaAggregate { private String name; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsCategoryStatusConverter.class) private NewsCategoryStatus status; private NewsCategory(){ } /* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return / public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category; } /* * 更新 * @param updater / public void update(NewsCategoryUpdater updater){ updater.accept(this); } /* * 启用 / public void enable(){ setStatus(NewsCategoryStatus.ENABLE); } /* * 禁用 / public void disable(){ setStatus(NewsCategoryStatus.DISABLE); } /* * 初始化,默认状态位 ENABLE / private void init() { setStatus(NewsCategoryStatus.ENABLE); }}3.2.2.7. 建模 NewsCategory 查找逻辑查找逻辑主要由 NewsCategoryRepository 完成。新建 NewsCategoryRepository,如下:/* * GenApplication 自动将该接口中的方法添加到 BaseNewsCategoryRepository 中 /@GenApplicationpublic interface NewsCategoryRepository extends BaseNewsCategoryRepository{ @Override Optional<NewsCategory> getById(Long aLong);}同样, BaseNewsCategoryRepository 也是自动生成的。interface BaseNewsCategoryRepository extends SpringDataRepositoryAdapter<Long, NewsCategory>, Repository<NewsCategory, Long>, QuerydslPredicateExecutor<NewsCategory> {}领域对象 NewsCategory 不应该暴露到其他层,因此,我们使用 DTO 模式处理数据的返回,新建 NewsCategoryDto,具体如下:public class NewsCategoryDto extends BaseNewsCategoryDto{ public NewsCategoryDto(NewsCategory source) { super(source); }}BaseNewsCategoryDto 为框架自动生成,如下:@Datapublic abstract class BaseNewsCategoryDto extends JpaAggregateVo implements Serializable { @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “status” ) private NewsCategoryStatus status; protected BaseNewsCategoryDto(NewsCategory source) { super(source); this.setName(source.getName()); this.setStatus(source.getStatus()); }}3.2.3. 构建 NewsCategoryApplication至此,领域的建模工作已经完成,让我们对 Application 进行构建。/* * GenController 自动将该类中的方法,添加到 BaseNewsCategoryController 中 /@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsCategoryController”)public interface NewsCategoryApplication extends BaseNewsCategoryApplication{ @Override NewsCategory create(NewsCategoryCreator creator); @Override void update(Long id, NewsCategoryUpdater updater); @Override void enable(Long id); @Override void disable(Long id); @Override Optional<NewsCategoryDto> getById(Long aLong);}自动生成的 BaseNewsCategoryApplication 如下:public interface BaseNewsCategoryApplication { Optional<NewsCategoryDto> getById(Long aLong); NewsCategory create(NewsCategoryCreator creator); void update(@Description(“主键”) Long id, NewsCategoryUpdater updater); void enable(@Description(“主键”) Long id); void disable(@Description(“主键”) Long id);}得益于我们的 EnableGenForAggregate 和 GenApplication 注解,BaseNewsCategoryApplication 包含我们想要的 Command 和 Query 方法。接口已经准备好了,接下来,处理实现类,具体如下:@Servicepublic class NewsCategoryApplicationImpl extends BaseNewsCategoryApplicationSupport implements NewsCategoryApplication { @Override protected NewsCategoryDto convertNewsCategory(NewsCategory src) { return new NewsCategoryDto(src); }}自动生成的 BaseNewsCategoryApplicationSupport 如下:abstract class BaseNewsCategoryApplicationSupport extends AbstractApplication implements BaseNewsCategoryApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private NewsCategoryRepository newsCategoryRepository; protected BaseNewsCategoryApplicationSupport(Logger logger) { super(logger); } protected BaseNewsCategoryApplicationSupport() { } protected NewsCategoryRepository getNewsCategoryRepository() { return this.newsCategoryRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } protected <T> List<T> convertNewsCategoryList(List<NewsCategory> src, Function<NewsCategory, T> converter) { if (CollectionUtils.isEmpty(src)) return Collections.emptyList(); return src.stream().map(converter).collect(Collectors.toList()); } protected <T> Page<T> convvertNewsCategoryPage(Page<NewsCategory> src, Function<NewsCategory, T> converter) { return src.map(converter); } protected abstract NewsCategoryDto convertNewsCategory(NewsCategory src); protected List<NewsCategoryDto> convertNewsCategoryList(List<NewsCategory> src) { return convertNewsCategoryList(src, this::convertNewsCategory); } protected Page<NewsCategoryDto> convvertNewsCategoryPage(Page<NewsCategory> src) { return convvertNewsCategoryPage(src, this::convertNewsCategory); } @Transactional( readOnly = true ) public <T> Optional<T> getById(Long aLong, Function<NewsCategory, T> converter) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(converter); } @Transactional( readOnly = true ) public Optional<NewsCategoryDto> getById(Long aLong) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(this::convertNewsCategory); } @Transactional public NewsCategory create(NewsCategoryCreator creator) { NewsCategory result = creatorFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .instance(() -> NewsCategory.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result; } @Transactional public void update(@Description(“主键”) Long id, NewsCategoryUpdater updater) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .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) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .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) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); }}该类中包含我们想要的所有实现。3.2.4. 构建 NewsCategoryControllerNewsInfoApplication 构建完成后,新建 NewsCategoryController 将其暴露出去。新建 NewsCategoryController, 如下:@RequestMapping(“news_category”)@RestControllerpublic class NewsCategoryController extends BaseNewsCategoryController{}是的,核心逻辑都在自动生成的 BaseNewsCategoryController 中:abstract class BaseNewsCategoryController { @Autowired private NewsCategoryApplication application; protected NewsCategoryApplication getApplication() { return this.application; } @ResponseBody @ApiOperation( value = “”, nickname = “create” ) @RequestMapping( value = “/_create”, method = RequestMethod.POST ) public ResultVo<NewsCategory> create(@RequestBody NewsCategoryCreator creator) { return ResultVo.success(this.getApplication().create(creator)); } @ResponseBody @ApiOperation( value = “”, nickname = “update” ) @RequestMapping( value = “{id}/_update”, method = RequestMethod.POST ) public ResultVo<Void> update(@PathVariable(“id”) Long id, @RequestBody NewsCategoryUpdater 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 = “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 = “getById” ) @RequestMapping( value = “/{id}”, method = RequestMethod.GET ) public ResultVo<NewsCategoryDto> getById(@PathVariable Long id) { return ResultVo.success(this.getApplication().getById(id).orElse(null)); }}3.2.5. 数据库准备至此,我们的代码就完全准备好了,现在需要准备建表语句。使用 Flyway 作为数据库的版本管理,在 resources/db/migration 新建 V1.002__create_news_category.sql 文件,具体如下:create table tb_news_category( id bigint auto_increment primary key, name varchar(32) null, status tinyint null, create_time bigint not null, update_time bigint not null, version tinyint not null);3.2.6. 测试至此,我们就完成了 NewsCategory 的开发。执行 maven 命令,启动项目:mvn clean spring-boot:run浏览器中输入 http://127.0.0.1:8090/swagger-ui.html , 通过 swagger 查看我们的成果。可以看到如下当然,可以使用 swagger 进行简单测试。3.3. NewsInfo 建模在 NewsCategory 的建模过程中,我们的主要精力放在了 NewsCategory 对象上,其他部分基本都是框架帮我们生成的。既然框架为我们做了那么多工作,为什么还需要我们新建 NewsCategoryApplication 和 NewsCategoryController呢?答案,需要为复杂逻辑预留扩展点。3.3.1. NewsInfo 建模整个过程,和 NewsCategory 基本一致,在此不在重复,只选择差异点进行说明。NewsInfo 最终代码如下:@EnableGenForAggregate@Index(“categoryId”)@Data@Entity@Table(name = “tb_news_info”)public class NewsInfo extends JpaAggregate { @Column(name = “category_id”, updatable = false) private Long categoryId; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsInfoStatusConverter.class) private NewsInfoStatus status; private String title; private String content; private NewsInfo(){ } /* * GenApplicationIgnore 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return / @GenApplicationIgnore public static NewsInfo create(Optional<NewsCategory> category, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo; } public void update(NewsInfoUpdater updater){ updater.accept(this); } public void enable(){ setStatus(NewsInfoStatus.ENABLE); } public void disable(){ setStatus(NewsInfoStatus.DISABLE); } private void init() { setStatus(NewsInfoStatus.ENABLE); }}3.3.1.1. NewsInfo 创建逻辑建模NewsInfo 的创建逻辑中,需要对 NewsCategory 的存在性和状态进行检查,只有存在并且状态为 ENABLE 才能添加 NewsInfo。具体实现如下:/* * GenApplicationIgnore 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return */@GenApplicationIgnorepublic static NewsInfo create(Optional<NewsCategory> category, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo;}该方法比较复杂,需要我们手工处理。在 NewsInfoApplication 中手工添加创建方法:@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsInfoController”)public interface NewsInfoApplication extends BaseNewsInfoApplication{ // 手工维护方法 NewsInfo create(Long categoryId, NewsInfoCreator creator);}在 NewsInfoApplicationImpl 添加实现:@Autowiredprivate NewsCategoryRepository newsCategoryRepository;@Overridepublic NewsInfo create(Long categoryId, NewsInfoCreator creator) { return creatorFor(getNewsInfoRepository()) .publishBy(getDomainEventBus()) .instance(()-> NewsInfo.create(this.newsCategoryRepository.getById(categoryId), creator)) .call();}其他部分不需要调整。3.3.2. NewsInfo 查找逻辑建模查找逻辑设计两个部分:根据 categoryId 进行分页查找;禁用的 NewsInfo 在查找中不可见。3.3.2.1. Index 注解在 NewsInfo 类上多了一个 @Index(“categoryId”) 注解,该注解会在 BaseNewsInfoRepository 中添加以 categoryId 为维度的查询。interface BaseNewsInfoRepository extends SpringDataRepositoryAdapter<Long, NewsInfo>, Repository<NewsInfo, Long>, QuerydslPredicateExecutor<NewsInfo> { Long countByCategoryId(Long categoryId); default Long countByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } List<NewsInfo> getByCategoryId(Long categoryId); List<NewsInfo> getByCategoryId(Long categoryId, Sort sort); default List<NewsInfo> getByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<NewsInfo> getByCategoryId(Long categoryId, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } Page<NewsInfo> findByCategoryId(Long categoryId, Pageable pageable); default Page<NewsInfo> findByCategoryId(Long categoryId, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); }}这样,并解决了第一个问题。3.3.2.2. 默认方法查看 NewsInfoRepository 类,如下:@GenApplicationpublic interface NewsInfoRepository extends BaseNewsInfoRepository{ default Page<NewsInfo> findValidByCategoryId(Long categoryId, Pageable pageable){ // 查找有效状态 Predicate valid = QNewsInfo.newsInfo.status.eq(NewsInfoStatus.ENABLE); return findByCategoryId(categoryId, valid, pageable); }}通过默认方法将业务概念转为为数据过滤。3.3.3. NewsInfo 数据库准备至此,整个结构与 NewsCategory 再无区别。最后,我们添加数据库文件 V1.003__create_news_info.sql :create table tb_news_info( id bigint auto_increment primary key, category_id bigint not null, status tinyint null, title varchar(64) not null, content text null, create_time bigint not null, update_time bigint not null, version tinyint not null);3.3.4. NewsInfo 测试启动项目,进行简单测试。4. 总结你用了多长时间完成整个系统呢?项目地址见:https://gitee.com/litao851025… ...

February 22, 2019 · 8 min · jiezi

扩展Spring Cloud Feign 实现自动降级

自动降级目的在Spring Cloud 使用feign 的时候,需要明确指定fallback 策略,不然会提示错误。 先来看默认的feign service 是要求怎么做的。feign service 定义一个 factory 和 fallback 的类@FeignClient(value = ServiceNameConstants.UMPS_SERVICE, fallbackFactory = RemoteLogServiceFallbackFactory.class)public interface RemoteLogService {}但是我们大多数情况的feign 降级策略为了保证幂等都会很简单,输出错误日志即可。类似如下代码,在企业中开发非常不方便@Slf4j@Componentpublic class RemoteLogServiceFallbackImpl implements RemoteLogService { @Setter private Throwable cause; @Override public R<Boolean> saveLog(SysLog sysLog, String from) { log.error(“feign 插入日志失败”, cause); return null; }}自动降级效果@FeignClient(value = ServiceNameConstants.UMPS_SERVICE)public interface RemoteLogService {}Feign Service 完成同样的降级错误输出FeignClient 中无需定义无用的fallbackFactoryFallbackFactory 也无需注册到Spring 容器中代码变化,去掉FeignClient 指定的降级工厂代码变化,删除降级相关的代码核心源码注入我们个性化后的Feign@Configuration@ConditionalOnClass({HystrixCommand.class, HystrixFeign.class})protected static class HystrixFeignConfiguration { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnProperty(“feign.hystrix.enabled”) public Feign.Builder feignHystrixBuilder(FeignContext feignContext) { return PigxHystrixFeign.builder(feignContext) .decode404() .errorDecoder(new PigxFeignErrorDecoder()); }}PigxHystrixFeign.target 方法是根据@FeignClient 注解生成代理类的过程,注意注释@Overridepublic <T> T target(Target<T> target) { Class<T> targetType = target.type(); FeignClient feignClient = AnnotatedElementUtils.getMergedAnnotation(targetType, FeignClient.class); String factoryName = feignClient.name(); SetterFactory setterFactoryBean = this.getOptional(factoryName, feignContext, SetterFactory.class); if (setterFactoryBean != null) { this.setterFactory(setterFactoryBean); } // 以下为获取降级策略代码,构建降级,这里去掉了降级非空的非空的校验 Class<?> fallback = feignClient.fallback(); if (fallback != void.class) { return targetWithFallback(factoryName, feignContext, target, this, fallback); } Class<?> fallbackFactory = feignClient.fallbackFactory(); if (fallbackFactory != void.class) { return targetWithFallbackFactory(factoryName, feignContext, target, this, fallbackFactory); } return build().newInstance(target);}构建feign 客户端执行PigxHystrixInvocationHandler的增强Feign build(@Nullable final FallbackFactory<?> nullableFallbackFactory) { super.invocationHandlerFactory((target, dispatch) -> new PigxHystrixInvocationHandler(target, dispatch, setterFactory, nullableFallbackFactory)); super.contract(new HystrixDelegatingContract(contract)); return super.build(); }PigxHystrixInvocationHandler.getFallback() 获取降级策略 @Override @Nullable @SuppressWarnings(“unchecked”) protected Object getFallback() { // 如果 @FeignClient 没有配置降级策略,使用动态代理创建一个 if (fallbackFactory == null) { fallback = PigxFeignFallbackFactory.INSTANCE.create(target.type(), getExecutionException()); } else { // 如果 @FeignClient配置降级策略,使用配置的 fallback = fallbackFactory.create(getExecutionException()); } }PigxFeignFallbackFactory.create 动态代理逻辑 public T create(final Class<?> type, final Throwable cause) { return (T) FALLBACK_MAP.computeIfAbsent(type, key -> { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(key); enhancer.setCallback(new PigxFeignFallbackMethod(type, cause)); return enhancer.create(); }); }PigxFeignFallbackMethod.intercept, 默认的降级逻辑,输出降级方法信息和错误信息,并且把错误格式public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) { log.error(“Fallback class:[{}] method:[{}] message:[{}]”, type.getName(), method.getName(), cause.getMessage()); if (R.class == method.getReturnType()) { final R result = cause instanceof PigxFeignException ? ((PigxFeignException) cause).getResult() : R.builder() .code(CommonConstants.FAIL) .msg(cause.getMessage()).build(); return result; } return null;}关注我们Spring Cloud 微服务开发核心包mica基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台 ...

February 22, 2019 · 2 min · jiezi

SpringBoot 实战 (十) | 声明式事务

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 SpringBoot 的 声明式事务。Spring 的事务机制所有的数据访问技术都有事务处理机制,这些技术提供了 API 用于开启事务、提交事务来完成数据操作,或者在发生错误时回滚数据。而 Spring 的事务机制是用统一的机制来处理不同数据访问技术的事务处理,Spring 的事务机制提供了一个 PlatformTransactionManager 接口,不同的数据访问技术的事务使用不同的接口实现,如下表:数据访问技术实现JDBCDataSourceTransactionManagerJPAJPATransactionManagerHibernateHibernateTransactionManagerJDOJdoTransactionManager分布式事务JtaTransactionManager声明式事务Spring 支持声明式事务,即使用注解来选择需要使用事务的方法,他使用 @Transactional 注解在方法上表明该方法需要事务支持。被注解的方法在被调用时,Spring 开启一个新的事务,当方法无异常运行结束后,Spring 会提交这个事务。如:@Transactionalpublic void saveStudent(Student student){ // 数据库操作}注意,@Transactional 注解来自于 org.springframework.transcation.annotation 包,而不是 javax.transaction。Spring 提供一个 @EnableTranscationManagement 注解在配置类上来开启声明式事务的支持。使用了 @EnableTranscationManagement 后,Spring 容器会自动扫描注解 @Transactional 的方法与类。@EnableTranscationManagement 的使用方式如下:@Configuration@EnableTranscationManagement public class AppConfig{}注解事务行为@Transactional 有如下表所示的属性来定制事务行为。属性含义propagation事务传播行为isolation事务隔离级别readOnly事务的读写性,boolean型timeout超时时间,int型,以秒为单位。rollbackFor一组异常类,遇到时回滚。(rollbackFor={SQLException.class})rollbackForCalssName一组异常类名,遇到回滚,类型为 string[]noRollbackFor一组异常类,遇到不回滚norollbackForCalssName一组异常类名,遇到时不回滚。类级别使用 @Transactional@Transactional 不仅可以注解在方法上,还可以注解在类上。注解在类上时意味着此类的所有 public 方法都是开启事务的。如果类级别和方法级别同时使用了 @Transactional 注解,则使用在类级别的注解会重载方法级别的注解。SpringBoot 的事务支持自动配置的事务管理器在使用 JDBC 作为数据访问技术时,配置定义如下:@Bean@ConditionalOnMissingBean@ConditionalOnBean(DataSource.class)public PlatformTransactionManager transactionManager(){ return new DataSourceTransactionManager(this.dataSource)}在使用 JPA 作为数据访问技术时,配置定义如下:@Bean@ConditionalOnMissingBean(PlatformTransactionManager.class)public PlatformTransactionManager transactionManager(){ return new JpaTransactionManager()}自动开启注解事务的支持SpringBoot 专门用于配置事务的类为 org.springframework.boot.autoconfigure.transcation.TransactionAutoConfiguration,此配置类依赖于 JpaBaseConfiguration 和 DataSourceTransactionManagerAutoConfiguration。而在 DataSourceTransactionManagerAutoConfiguration 配置里还开启了对声明式事务的支持,代码如下:@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)@Configuration@EnableTransactionManagementprotected static class TransactionManagementConfiguration{}所以在 SpringBoot 中,无须显式开启使用 @EnableTransactionManagement 注解。实战演示如何使用 Transactional 使用异常导致数据回滚与使用异常导致数据不回滚。准备工作:SpringBoot 2.1.3JDK 1.8IDEApom.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.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>transaction</artifactId> <version>0.0.1-SNAPSHOT</version> <name>transaction</name> <description>transaction Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!– JPA 相关 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!– web 启动类 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mysql 连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– lombok 插件,简化实体代码 –> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</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>代码注释很清楚,没啥好说的。application.yaml 配置:spring: # \u6570\u636E\u5E93\u76F8\u5173 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: root password: 123456 # jpa \u76F8\u5173 jpa: hibernate: ddl-auto: update # ddl-auto:\u8BBE\u4E3A create \u8868\u793A\u6BCF\u6B21\u90FD\u91CD\u65B0\u5EFA\u8868 show-sql: true实体类:import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@Entity@AllArgsConstructor@NoArgsConstructorpublic class Student { @Id @GeneratedValue private Integer id; private String name; private Integer age;}dao 层import com.nasus.transaction.domain.Student;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;@Repositorypublic interface StudentRepository extends JpaRepository<Student, Integer> {}service 层import com.nasus.transaction.domain.Student;public interface StudentService { Student saveStudentWithRollBack(Student student); Student saveStudentWithoutRollBack(Student student);}实现类:import com.nasus.transaction.domain.Student;import com.nasus.transaction.repository.StudentRepository;import com.nasus.transaction.service.StudentService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class StudentServiceImpl implements StudentService { @Autowired // 直接注入 StudentRepository 的 bean private StudentRepository studentRepository; // 使用 @Transactional 注解的 rollbackFor 属性,指定特定异常时,触发回滚 @Transactional(rollbackFor = {IllegalArgumentException.class}) @Override public Student saveStudentWithRollBack(Student student) { Student s = studentRepository.save(student); if (“高斯林”.equals(s.getName())){ //硬编码,手动触发异常 throw new IllegalArgumentException(“高斯林已存在,数据将回滚”); } return s; } // 使用 @Transactional 注解的 noRollbackFor 属性,指定特定异常时,不触发回滚 @Transactional(noRollbackFor = {IllegalArgumentException.class}) @Override public Student saveStudentWithoutRollBack(Student student) { Student s = studentRepository.save(student); if (“高斯林”.equals(s.getName())){ throw new IllegalArgumentException(“高斯林已存在,数据将不会回滚”); } return s; }}代码注释同样很清楚,没啥好说的。controller 层import com.nasus.transaction.domain.Student;import com.nasus.transaction.service.StudentService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/student”)public class StudentController { // 注入 studentservice 的 bean @Autowired private StudentService studentService; // 测试回滚情况 @PostMapping("/withRollBack”) public Student saveStudentWithRollBack(@RequestBody Student student){ return studentService.saveStudentWithRollBack(student); } // 测试不回滚情况 @PostMapping("/withOutRollBack”) public Student saveStudentWithoutRollBack(@RequestBody Student student){ return studentService.saveStudentWithoutRollBack(student); }}Postman 测试结果为了更清楚地理解回滚,以 debug (调试模式) 启动程序。并在 StudentServiceImpl 的 saveStudentWithRollBack 方法上打上断点。测试前数据库结果:Postman 测试回滚debug 模式下可见数据已保存,且获得 id 为 1。:继续执行抛出异常 IllegalArgumentException,将导致数据回滚:测试后数据库结果:并无新增数据,回滚成功。Postman 测试不回滚测试前数据库结果:遇到 IllegalArgumentException 异常数据不会回滚:测试后数据库结果:新增数据,数据不回滚。源码下载https://github.com/turoDog/Demo/tree/master/springboot_transaction_demo后语以上为 SpringBoot 声明式事务的教程。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 21, 2019 · 2 min · jiezi

实战:基于Spring Boot快速开发RESTful风格API接口

写在前面的话这篇文章计划是在过年期间完成的,示例代码都写好了,结果亲戚来我家做客,文章没来得及写。已经很久没有更新文章了,小伙伴们,有没有想我啊。言归正传,下面开始,今天的话题。目标写一套符合规范,并且具有RESTful风格的API接口。假定你已会使用Spring Boot 2.x。你已会使用Gradle构建Spring Boot工程。你已会基于Spring Boot编写API接口。你已会使用接口调试工具。如果你还不会使用Spring Boot写接口,建议先看一下这篇文章 :用Spring Boot开发API接口步骤1、基于Gradle构建Spring Boot示例项目。2、引入JavaLib。3、编写接口代码。4、测试接口。引入JavaLib测试版(SNAPSHOT),都会发布到 JitPack 上,所以,从这里拉取的,都会是最新的,但是需要配置仓库地址。正式版(RELEASE),才会推送到 Maven中央。UserModel我们用UserModel来存放我们的数据,以便存取。我个人比较喜欢用bean的,如果你喜欢用Map,那也是可以的。不过需要注意的是,需要加@JsonInclude(JsonInclude.Include.NON_NULL) ,他的作用是,如果某个字段为空时,在返回的JSON中,则不显示,如果没有,将为 null。完整代码如下:package com.fengwenyi.demojavalibresult.model;import com.fasterxml.jackson.annotation.JsonInclude;import lombok.Data;import lombok.experimental.Accessors;import java.io.Serializable;/** * User Model * @author Wenyi Feng * @since 2019-02-05 /@Data@Accessors(chain = true)@JsonInclude(JsonInclude.Include.NON_NULL)public class UserModel implements Serializable { private static final long serialVersionUID = -835481508750383832L; /* UID / private String uid; /* Name / private String name; /* Age / private Integer age;}编写接口返回码这里我们使用 JavaLib 中result模块为我们提供的方法。只需要调用 BaseCodeMsg.app(Integer, String)即可。这里我们只写几个用作示例,完整代码如下:package com.fengwenyi.demojavalibresult.util;import com.fengwenyi.javalib.result.BaseCodeMsg;/* * 自定义返回码以及描述信息 * @author Wenyi Feng * @since 2019-02-05 /public class CodeMsg { / user error ————————————————————————————————————*/ /** 用户不存在 / public static final BaseCodeMsg ERROR_USER_NOT_EXIST = BaseCodeMsg.app(10001, “User Not Exist”); /* UID不能为空 / public static final BaseCodeMsg ERROR_USER_UID_NOT_NULL = BaseCodeMsg.app(10002, “User UID Must Not null”);}BaseCodeMsg我们看一下源码:package com.fengwenyi.javalib.result;/* * (基类)返回码及描述信息 * @author Wenyi Feng * @since 2019-01-22 /public class BaseCodeMsg { /* 返回码 / private Integer code; /* 返回码描述 / private String msg; /* * 无参数构造方法 / private BaseCodeMsg() {} /* * 构造方法 * @param code * @param msg / private BaseCodeMsg(Integer code, String msg) { this.code = code; this.msg = msg; } public static BaseCodeMsg app(Integer code, String msg) { return new BaseCodeMsg(code, msg); } /* * 返回码填充 * @param args 填充内容 * @return CodeMsgEnum / public BaseCodeMsg fillArgs(Object … args) { this.msg = String.format(this.msg, args); return this; } /* * 获取返回码 * @return 返回码 / public Integer getCode() { return code; } /* * 获取描述信息 * @return 描述信息 / public String getMsg() { return msg; } /* 成功 / public static final BaseCodeMsg SUCCESS = BaseCodeMsg.app(0, “Success”); /* 失败 / public static final BaseCodeMsg ERROR_INIT = BaseCodeMsg.app(-1, “Error”);}成功的标识是:当 code=0 时。另外,我们还为你提供了预留字符串替换的方法。比如你想告诉用户某个字段不合法,那么你可以这样:第一步:在CodeMsg中添加public static final BaseCodeMsg ERROR_PARAM_ILLEGAL = BaseCodeMsg.app(20001, “Request Param Illegal : %s”);第二步:返回 /* * 测试参数错误 * @return {@link Result} / @GetMapping("/test-param-error") public Result testParamError() { return Result.error(CodeMsg.ERROR_PARAM_ILLEGAL.fillArgs(“account”)); }测试结果:编写接口代码接下来,开始编写我们的接口代码。首先指明,我们的接口接收和返回的文档格式。consumes = MediaType.APPLICATION_JSON_UTF8_VALUEproduces = MediaType.APPLICATION_JSON_UTF8_VALUE再使用 JavaLib 中 Result。完整代码如下:package com.fengwenyi.demojavalibresult.controller;import com.fengwenyi.demojavalibresult.model.UserModel;import com.fengwenyi.demojavalibresult.util.CodeMsg;import com.fengwenyi.javalib.result.Result;import org.springframework.http.MediaType;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.;import javax.annotation.PostConstruct;import java.util.ArrayList;import java.util.List;import java.util.UUID;/** * User Controller : 用户操作 * @author Wenyi Feng * @since 2019-02-05 /@RestController@RequestMapping(value = “/user”, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public class UserController { /* 临时存放用户信息 / private List<UserModel> userModelList = new ArrayList<>(); /* * 初始化用户 / @PostConstruct public void init() { for (int i = 0; i < 10; i++) userModelList.add(new UserModel().setUid(UUID.randomUUID().toString()).setName(“u” + i).setAge(10 + i)); } /* * 查询用户列表 * @return {@link Result} / @GetMapping("/list") public Result list() { return Result.success(userModelList); } /* * 添加用户 * @param userModel 这里传JSON字符串 * @return {@link Result} / @PostMapping("/add") public Result add(@RequestBody UserModel userModel) { if (userModel != null) { userModelList.add(userModel.setUid(UUID.randomUUID().toString())); return Result.success(); } return Result.error(); } /* * 根据UID获取用户 * @param uid UID * @return {@link Result} */ @GetMapping("/get/{uid}") public Result getByUid(@PathVariable(“uid”) String uid) { if (StringUtils.isEmpty(uid)) return Result.error(CodeMsg.ERROR_USER_UID_NOT_NULL); for (UserModel userModel : userModelList) if (userModel.getUid().equals(uid)) return Result.success(userModel); return Result.error(CodeMsg.ERROR_USER_NOT_EXIST); }}测试1、启动2、list访问:http://localhost:8080/user/list{ “code”: 0, “msg”: “Success”, “data”: [ { “uid”: “d8e2dfac-b6e8-46c7-9d43-5bb6bf99ce30”, “name”: “u0”, “age”: 10 }, { “uid”: “87001637-9f21-4bc7-b589-bea1b2c795c4”, “name”: “u1”, “age”: 11 }, { “uid”: “5e1398ca-8322-4a68-b0d2-1eb4c1cac9de”, “name”: “u2”, “age”: 12 }, { “uid”: “e6ee5452-4148-4f6d-b820-9cc24e5c91b5”, “name”: “u3”, “age”: 13 }, { “uid”: “3f428e26-57e1-4661-8275-ce3777b5da54”, “name”: “u4”, “age”: 14 }, { “uid”: “b9d994b4-f090-40de-b0f3-e89c613061f2”, “name”: “u5”, “age”: 15 }, { “uid”: “748d1349-5978-4746-b0c1-949eb5613a28”, “name”: “u6”, “age”: 16 }, { “uid”: “abaadb7c-23fb-4297-a531-0c490927f6d5”, “name”: “u7”, “age”: 17 }, { “uid”: “5e5917a1-8674-4367-94c6-6a3fd10a08d6”, “name”: “u8”, “age”: 18 }, { “uid”: “03ed6a83-0cc0-4714-9d0d-f653ebb3a2eb”, “name”: “u9”, “age”: 19 } ]}2、添加数据看一下,数据是什么样子与我们预想的结果一样。获取数据有数据样式:无数据样式:关于冯文议。2017年毕业于阿坝师范学院计算机应用专业。现就职于深圳警圣技术股份有限公司,主要负责服务器接口开发工作。技术方向:Java。 开源软件:JavaLib。后记到这里就结束了,如果在遇到什么问题,或者有不明白的地方,可以通过评论、留言或者私信等方式,告诉我。 ...

February 21, 2019 · 3 min · jiezi

SpringBoot整合Kotlin构建Web服务

今天我们尝试Spring Boot整合Kotlin,并决定建立一个非常简单的Spring Boot微服务,使用Kotlin作为编程语言进行编码构建。创建一个简单的Spring Boot应用程序。我会在这里使用maven构建项目:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://maven.apache.org/POM/4.0.0" 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.edurt.ski</groupId> <artifactId>springboot-kotlin-integration</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>springboot kotlin integration</name> <description>SpringBoot Kotlin Integration is a open source springboot, kotlin integration example.</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.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> <!– plugin config –> <plugin.maven.kotlin.version>1.2.71</plugin.maven.kotlin.version> </properties> <dependencies> <!– spring boot –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– kotlin –> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> </dependencies> <build> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <compilerPlugins> <plugin>spring</plugin> <plugin>jpa</plugin> <plugin>all-open</plugin> </compilerPlugins> <pluginOptions> <option>all-open:annotation=javax.persistence.Entity</option> </pluginOptions> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${plugin.maven.kotlin.version}</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> <version>${plugin.maven.kotlin.version}</version> </dependency> </dependencies> <executions> <execution> <id>kapt</id> <goals> <goal>kapt</goal> </goals> <configuration> <sourceDirs> <sourceDir>src/main/kotlin</sourceDir> </sourceDirs> <annotationProcessorPaths> <annotationProcessorPath> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${project.parent.version}</version> </annotationProcessorPath> </annotationProcessorPaths> </configuration> </execution> </executions> </plugin> </plugins> </build></project>添加所有必需的依赖项:kotlin-stdlib-jdk8 kotlin jdk8的lib包kotlin-reflect kotlin反射包一个简单的应用类:package com.edurt.skiimport org.springframework.boot.autoconfigure.SpringBootApplicationimport org.springframework.boot.runApplication@SpringBootApplicationclass SpringBootKotlinIntegrationfun main(args: Array<String>) { runApplication<SpringBootKotlinIntegration>(args)}添加Rest API接口功能创建一个HelloController Rest API接口,我们只提供一个简单的get请求获取hello,kotlin输出信息:package com.edurt.ski.controllerimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.RestController@RestControllerclass HelloController { @GetMapping(value = “hello”) fun hello(): String { return “hello,kotlin” }}修改SpringBootKotlinIntegration文件增加以下设置扫描路径@ComponentScan(value = [ “com.edurt.ski”, “com.edurt.ski.controller”])添加页面功能修改pom.xml文件增加以下页面依赖<!– mustache –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mustache</artifactId></dependency>在src/main/resources路径下创建templates文件夹在templates文件夹下创建一个名为hello.mustache的页面文件<h1>Hello, Kotlin</h1>创建页面转换器HelloViewpackage com.edurt.ski.viewimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMapping@Controllerclass HelloView { @GetMapping(value = “hello_view”) fun helloView(model: Model): String { return “hello” }}浏览器访问http://localhost:8080/hello_view即可看到页面内容添加数据持久化功能修改pom.xml文件增加以下依赖(由于测试功能我们使用h2内存数据库)<!– data jpa and db –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope></dependency>创建User实体package com.edurt.ski.modelimport javax.persistence.Entityimport javax.persistence.GeneratedValueimport javax.persistence.Id@Entity//class UserModel(// @Id// @GeneratedValue// private var id: Long? = 0,// private var name: String//)class UserModel { @Id @GeneratedValue var id: Long? = 0 get() = field set var name: String? = null get() = field set}创建UserSupport dao数据库操作工具类package com.edurt.ski.supportimport com.edurt.ski.model.UserModelimport org.springframework.data.repository.PagingAndSortingRepositoryinterface UserSupport : PagingAndSortingRepository<UserModel, Long> {}创建UserService服务类package com.edurt.ski.serviceimport com.edurt.ski.model.UserModelinterface UserService { /* * save model to db / fun save(model: UserModel): UserModel}创建UserServiceImpl实现类package com.edurt.ski.serviceimport com.edurt.ski.model.UserModelimport com.edurt.ski.support.UserSupportimport org.springframework.stereotype.Service@Service(value = “userService”)class UserServiceImpl(private val userSupport: UserSupport) : UserService { override fun save(model: UserModel): UserModel { return this.userSupport.save(model) }}创建用户UserController进行持久化数据package com.edurt.ski.controllerimport com.edurt.ski.model.UserModelimport com.edurt.ski.service.UserServiceimport org.springframework.web.bind.annotation.PathVariableimport org.springframework.web.bind.annotation.PostMappingimport org.springframework.web.bind.annotation.RequestMappingimport org.springframework.web.bind.annotation.RestController@RestController@RequestMapping(value = “user”)class UserController(private val userService: UserService) { @PostMapping(value = “save/{name}”) fun save(@PathVariable name: String): UserModel { val userModel = UserModel()// userModel.id = 1 userModel.name = name return this.userService.save(userModel) }}使用控制台窗口执行以下命令保存数据curl -X POST http://localhost:8080/user/save/qianmoQ收到返回结果{“id”:1,“name”:“qianmoQ”}表示数据保存成功增加数据读取渲染功能修改UserService增加以下代码/* * get all model */fun getAll(page: Pageable): Page<UserModel>修改UserServiceImpl增加以下代码override fun getAll(page: Pageable): Page<UserModel> { return this.userSupport.findAll(page)}修改UserController增加以下代码@GetMapping(value = “list”)fun get(): Page<UserModel> = this.userService.getAll(PageRequest(0, 10))创建UserView文件渲染User数据package com.edurt.ski.viewimport com.edurt.ski.service.UserServiceimport org.springframework.data.domain.PageRequestimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.ui.setimport org.springframework.web.bind.annotation.GetMapping@Controllerclass UserView(private val userService: UserService) { @GetMapping(value = “user_view”) fun helloView(model: Model): String { model[“users”] = this.userService.getAll(PageRequest(0, 10)) return “user” }}创建user.mustache文件渲染数据(自行解析返回数据即可){{users}}浏览器访问http://localhost:8080/user_view即可看到页面内容增加单元功能修改pom.xml文件增加以下依赖<!– test –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> <exclusion> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> </exclusion> </exclusions></dependency><dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <scope>test</scope></dependency>创建UserServiceTest文件进行测试UserService功能package com.edurt.skiimport com.edurt.ski.service.UserServiceimport org.junit.jupiter.api.AfterAllimport org.junit.jupiter.api.Testimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.boot.test.context.SpringBootTestimport org.springframework.data.domain.PageRequest@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class UserServiceTest(@Autowired private val userService: UserService) { @Test fun get all() { println(">> Assert blog page title, content and status code”) val entity = this.userService.getAll(PageRequest(0, 1)) print(entity.totalPages) }}源码地址:GitHub ...

February 20, 2019 · 3 min · jiezi

基于 CODING 的 Spring Boot 持续集成项目

本文作者:CODING 用户 - 廖石荣持续集成的概念持续集成(Continuous integration,简称 CI)是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。持续集成的模式如图所示:CI 过程:代码编写 -> 源代码库(GitHub or gitlab)-> CI 服务器(代码构建、自动化测试、结果反馈【构建结果】)涉及 CI 工具:Jenkins、Travis CI、TeamCity、Gitlab CI、CircleCI、Codeship 等,相关资料可以查询对应的官网,其中应用广泛的 Jenkins 和 Travis CI,市场上也推出了智能化的持续集成服务商,比如「CODING 持续集成」,它是基于 Jenkins 配置集成服务,真正实现了一键提交代码,持续集成,部署服务。持续集成的优点1.解放了重复性劳动。自动化部署工作可以解放集成、测试、部署等重复性劳动,而机器集成的频率明显比手工高很多。2.更快地修复问题。持续集成更早的获取变更,更早的进入测试,更早的发现问题,解决问题的成本显著下降。3.更快的交付成果。更早发现错误减少解决错误所需的工作量。集成服务器在构建环节发现错误可以及时通知开发人员修复。集成服务器在部署环节发现错误可以回退到上一版本,服务器始终有一个可用的版本。4.减少手工的错误。在重复性动作上,人容易犯错,而机器犯错的几率几乎为零。5.减少了等待时间。缩短了从开发、集成、测试、部署各个环节的时间,从而也就缩短了中间可以出现的等待时机。持续集成,意味着开发、集成、测试、部署也得以持续。6.更高的产品质量。集成服务器往往提供代码质量检测等功能,对不规范或有错误的地方会进行标致,也可以设置邮件和短信等进行警告。持续集成服务的选择关于网上集成服务的工具很多,其中尤其以 Jenkins 服务最受欢迎,但是 Jenkins 服务需要在自己服务器上进行配置安装,以及安装各种插件,对于刚上手的小白来说,可能存在一定的门槛,操作步骤繁多,操作不够智能,不是真正的自动化运维,缺少一键发布构建服务。所以我们选择了「CODING 持续集成」。CODING 提供的集成服务是什么「CODING 持续集成」是基于 Jenkins 的,兼容 Jenkinsfile 配置文件,如果您之前有使用过或者写过 Jenkinsfile 相信您会很快上手。如何使用CODING持续集成服务「CODING 持续集成」是基于 Jenkins 的,通过 Jenkinsfile 配置文件完成 CI 的步骤,接下来将引导您一步步创建一个持续集成示例。登录 CODING,进入项目中心,点击左边菜单集成服务,开通集成服务,配置完成之后会手动触发第一次构建过程。找到或者创建 Jenkinsfile,如果你对于 Jenkins 比较熟悉的话,可以自己编写 Jenkinsfile 配置文件,也可以采用 CODING 提供的模板文件,如下我就采用了 Jenkinsfile 模板文件来实行自动化持续集成服务,您可以在修改 Jenkinsfile 的时候修改触发方式,您可以自行选择是推送到某个标签或者某个分支时间触发构建。Jenkins 以及能够为 agent 默认配置好 timezone 和 localtime (默认中国上海)。配置好 Jenkinsfile 文件以及配置好环境变量,点击保存,便可以进行持续集成项目了。如图所示,集成步骤分为拉取代码-》构建-》测试-》部署等步骤,点击每个步骤可以看到相应的命令执行情况,下面来一个一个步骤配合 Jenkinsfile 文件解释命令的一些执行情况:代码工程结构如图所示:1.检出项目,如下所示 Jenkinsfile 配置文件第一步通过 Git 检出在远程仓库分支的代码,至于哪个分支可以通过环境变量配置读取 REF 这个环境变量stage(“检出”) { steps { sh ‘ci-init’ checkout( [$class: ‘GitSCM’, branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[url: env.GIT_REPO_URL]]] ) } }如上图所示,第一步主要是执行从 Git 仓库远程拉取代码,所以命令都是 Git 里面的,包括读取 Git 配置的环境变量包括更新 Jenkinsfile 文件2.构建项目,如下命令所示构建这一步主要是初始化代码和打包代码,因为我们这个工程是以 Java 为主要开发语言,所以重点关注 Java 版本和安装 Maven 命令即可打包,目前 CODING 提供的语言环境包括了 java-1.8.0_181, go-1.7.4, node-10.11.0, php-7.0.30, ruby-2.3, python-2.7.13 等。如有需要可以联系客服开通其它语言环境。stage(“构建”) { steps { echo “构建中…” sh ‘go version’ sh ’node -v’ sh ‘java -version’ // sh ‘php -v’ // sh ‘python -V’ // sh ‘gcc -v’ // sh ‘make -v’ // 请在这里放置您项目代码的单元测试调用过程,例如: sh ‘mvn clean’ // mvn 清除缓存 sh ‘mvn install’ // 构建 Maven 工程 // sh ‘make’ // make 示例 echo “构建完成.” // archiveArtifacts artifacts: ‘**/target/.jar’, fingerprint: true // 收集构建产物 } }因为这个 SpringBoot 项目是以 Java 为主的项目,所以在 Jenkinsfile 文件命令里面其实可以把其它语言的检查版本命令去掉,只需要执行 java -version 命令即可。第一次构建失败:如上图所示,第一次执行执行构建 jar 包失败,因为在本地可以正常 mvn install,所以起初我百思不得其解,上网找了很多资料,经过多番查找,最后在 Stack Overflow 找到了答案,这是由于 OpenJDK 1.8.0_181 这个版本中存在的一个 bug 所致,原文如下:链接,最终解决方案采用更改 pom.xml 文件:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <useSystemClassLoader>false</useSystemClassLoader> <skipTests>true</skipTests> </configuration></plugin>成功构建结果如下:3.测试项目,如下所示,我们 SpringBoot 工程通过 mvn test 测试命令即可,比如下面我们测试其中一个用户信息相关的单元测试:stage(“测试”) { steps { echo “单元测试中…” // 请在这里放置您项目代码的单元测试调用过程,例如: sh ‘mvn test -Dtest=com.my.segmentfault.website.Pwdtest’ //测试其中一个单元测试 echo “单元测试完成.” } }第一次失败测试结果如下:后来经检查,是单元测试代码其中存在 bug,修正之后,正确的第二次测试结果如下:4.部署项目,如下所示,部署项目命令可以执行自己写的部署脚本文件。各位可以结合自己项目的真实环境,编写简单的部署脚本,比如上传 jar 包到服务器,然后通过 java - jar XXXX.jar 包执行方式,以及上传 war 包到 tomcat 服务器,然后启动 tomcat 服务器等,也可以结合自己公司项目需要编写复杂的执行脚本文件,然后调用执行脚本命令,比如下面举一个简单的执行脚本例子。部署命令:stage(“部署”) { steps { echo “部署中…” sh ‘./deploy.sh start’ // 启动 tomcat 服务 // sh ‘./deploy.sh stop’ // 停止 tomcat 服务 echo “部署完成” } }deploy.sh 脚本:(其中一些 tomcat 服务路径配置根据自己需要进行修改)#!/bin/bash tomcat_home=/usr/tomcat/apache-tomcat-8.0.48 //修改为自己服务器的 tomcat 路径SHUTDOWN=$tomcat_home/bin/shutdown.sh STARTTOMCAT=$tomcat_home/bin/startup.sh case $1 instart) echo “启动$tomcat_home”$STARTTOMCAT ;; stop) echo “关闭$tomcat_home”$SHUTDOWN pidlist=ps -ef |grep tomcat |grep -v "grep"|awk '{print $2}' kill -9 $pidlist #!/bin/bash tomcat_home=/usr/tomcat/apache-tomcat-8.0.48 SHUTDOWN=$tomcat_home/bin/shutdown.sh STARTTOMCAT=$tomcat_home/bin/startup.sh case $1 instart) echo “启动$tomcat_home”$STARTTOMCAT ;; stop) echo “关闭$tomcat_home”$SHUTDOWN pidlist=ps -ef |grep tomcat |grep -v "grep"|awk '{print $2}' kill -9 $pidlist stop) echo “关闭$tomcat_home”$SHUTDOWN pidlist=ps -ef |grep tomcat |grep -v "grep"|awk '{print $2}' kill -9 $pidlist #删除日志文件,如果你不先删除可以不要下面一行 rm $tomcat_home/logs/ -rf #删除tomcat的临时目录 rm $tomcat_home/work/* -rf ;; restart) echo “关闭$tomcat_home”$SHUTDOWN pidlist=ps -ef |grep tomcat |grep -v "grep"|awk '{print $2}' kill -9 $pidlist #删除日志文件,如果你不先删除可以不要下面一行 rm $tomcat_home/logs/* -rf #删除tomcat的临时目录 rm $tomcat_home/work/* -rf sleep 5 echo “启动$tomcat_home”$STARTTOMCAT #看启动日志 #tail -f $tomcat_home/logs/catalina.out ;; logs) cd /mnt/alidata/apache-tomcat-7.0.68/logstail -f catalina.out ;; esac 服务启动展示系统主页如下图所示:文章详情如下图所示:归档页面如下图所示:系统后台管理如图所示:总结CODING 是一个面向开发者的云端开发平台,提供 Git/SVN 代码托管、任务管理、在线 WebIDE、Cloud Studio、开发协作、文件管理、Wiki 管理、提供个人服务及企业服务,其中实现了 DevOps 流程全自动化,为企业提供软件研发全流程管理工具,打通了从团队构建、产品策划、开发测试到部署上线的全过程。「CODING 持续集成」集成了 Jenkins 等主流企业开发流程工具,如上所示,这个以 SpringBoot 打造的 CMS 社区系统便可以在 CODING 上面实现团队协作开发,一键部署作为团队以及公司文档共享社区论坛等作用。本文适量引用:“持续集成”词条的百度百科 ...

February 20, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App(六)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章将给聊天App_PigChat加上心跳机制。为什么要实现心跳机制如果没有特意的设置某些选项或者实现应用层心跳包,TCP空闲的时候是不会发送任何数据包。也就是说,当一个TCP的socket,客户端与服务端谁也不发送数据,会一直保持着连接。这其中如果有一方异常掉线(例如死机、路由被破坏、防火墙切断连接等),另一端如果没有发送数据,永远也不可能知道。这对于一些服务型的程序来说,是灾难性的后果,将会导致服务端socket资源耗尽。举个简单的例子,当我们因为特殊情况打开飞行模式 ,在处理完事件之后再关闭飞行模式,这时候如果再进入应用程序中,我们将以新的channel进入,但是之前的channel还是会保留。因此,为了保证连接的有效性、及时有效地检测到一方的非正常断开,保证连接的资源被有效的利用,我们就会需要一种保活的机制,通常改机制两种处理方式:1、利用TCP协议层实现的Keepalive;2、自己在应用层实现心跳包。实现心跳机制新建一个HeartBeatHandler用于检测channel的心跳。继承ChannelInboundHandlerAdapter,并重写其userEventTriggered方法。当客户端的所有ChannelHandler中4s内没有write事件,则会触发userEventTriggered方法。首先我们判断evt是否是IdleStateEvent的实例,IdleStateEvent用于触发用户事件,包含读空闲/写空闲/读写空闲。对evt进行强制履行转换后,通过state判断其状态,只有当其该channel处于读写空闲的时候才将这个channel关闭。/** * @Description: 用于检测channel的心跳handler * 继承ChannelInboundHandlerAdapter,从而不需要实现channelRead0方法 */public class HeartBeatHandler extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // 判断evt是否是IdleStateEvent(用于触发用户事件,包含 读空闲/写空闲/读写空闲 ) if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent)evt; // 强制类型转换 if (event.state() == IdleState.READER_IDLE) { System.out.println(“进入读空闲…”); } else if (event.state() == IdleState.WRITER_IDLE) { System.out.println(“进入写空闲…”); } else if (event.state() == IdleState.ALL_IDLE) { System.out.println(“channel关闭前,users的数量为:” + ChatHandler.users.size()); Channel channel = ctx.channel(); // 关闭无用的channel,以防资源浪费 channel.close(); System.out.println(“channel关闭后,users的数量为:” + ChatHandler.users.size()); } } } }增加心跳支持在原来的WSServerInitialzer中增加心跳机制的支持。 // ====================== 增加心跳支持 start ====================== // 针对客户端,如果在1分钟时没有向服务端发送读写心跳(ALL),则主动断开 // 如果是读空闲或者写空闲,不处理 pipeline.addLast(new IdleStateHandler(8, 10, 12)); // 自定义的空闲状态检测 pipeline.addLast(new HeartBeatHandler()); // ====================== 增加心跳支持 end ====================== ...

February 19, 2019 · 1 min · jiezi

Intellij IDEA 开发 Spring-boot项目 热部署,自动部署

使用Intellij IDEA 开发 Spring-boot项目 热部署,自动部署使用Intellij IDEA 开发 Spring-boot项目,即使项目使用了spring-boot-devtools,修改了类或者html、js等,idea还是不会自动重启,非要手动去make一下或者重启,就更没有使用热部署一样。网上关于spring-boot-devtools的热部署都是eclipse的配置,并不适合IDEA,IDEA的需要特殊的设置首先,IDEA设置里面这里必须打勾 然后 Shift+Ctrl+Alt+/,选择Registry ok了,重启一下项目,然后改一下类里面的内容,IDEA就会自动去make了。

February 19, 2019 · 1 min · jiezi

Spring Boot MyBatis配置多种数据库

mybatis-config.xml是支持配置多种数据库的,本文将介绍在Spring Boot中使用配置类来配置。1. 配置application.yml# mybatis配置mybatis: check-config-location: false type-aliases-package: ${base.package}.model configuration: map-underscore-to-camel-case: true # 二级缓存的总开关 cache-enabled: false mapper-locations: classpath:mapping/*.xml2. 新增数据源配置类/** * 数据源配置 * @author simon * @date 2019-02-18 /@Configurationpublic class DataSourceConfig { @Value("${mybatis.mapper-locations}") private String mapperLocations; @Primary @Bean @ConfigurationProperties(“spring.datasource.druid”) public DataSource dataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean public JdbcTemplate jdbcTemplate(){ return new JdbcTemplate(dataSource()); } @Bean public DatabaseIdProvider databaseIdProvider(){ DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); Properties p = new Properties(); p.setProperty(“Oracle”, “oracle”); p.setProperty(“MySQL”, “mysql”); p.setProperty(“PostgreSQL”, “postgresql”); p.setProperty(“DB2”, “db2”); p.setProperty(“SQL Server”, “sqlserver”); databaseIdProvider.setProperties(p); return databaseIdProvider; } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); factoryBean.setDatabaseIdProvider(databaseIdProvider()); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); return factoryBean; }}3. 在mapper.xml中使用方法1 <select id=“findAuthorityByUrl” resultType=“java.lang.String” databaseId=“mysql”> SELECT group_concat( tsma.authority ) as authority FROM t_side_menu tsm LEFT JOIN t_side_menu_authority tsma ON tsm.id = tsma.side_menu_id </select> <select id=“findAuthorityByUrl” resultType=“java.lang.String” databaseId=“postgresql”> SELECT string_agg( tsma.authority, ‘,’) as authority FROM t_side_menu tsm LEFT JOIN t_side_menu_authority tsma ON tsm.id = tsma.side_menu_id </select>方法2 <select id=“selectByPids” parameterType=“String” resultMap=“SuperResultMap”> SELECT tsm., <if test="_databaseId == ‘mysql’"> group_concat( tsma.authority ) as authority </if> <if test="_databaseId == ‘postgresql’"> string_agg( tsma.authority, ‘,’) as authority </if> FROM t_side_menu tsm LEFT JOIN t_side_menu_authority tsma ON tsm.id = tsma.side_menu_id WHERE pid IN (#{pids}) GROUP BY tsm.id </select>题外话如果有兴趣,请给oauthserer项目一个star。oauthserver是一个基于Spring Boot Oauth2的完整的独立的Oauth2 Server微服务。项目的目的是,仅仅需要创建相关数据表,修改数据库的连接信息,你就可以得到一个Oauth2 Server微服务。 ...

February 18, 2019 · 1 min · jiezi

Spring Security整合KeyCloak保护Rest API

今天我们尝试Spring Security整合Keycloak,并决定建立一个非常简单的Spring Boot微服务,使用Keycloak作为我的身份验证源,使用Spring Security处理身份验证和授权。设置Keycloak首先我们需要一个Keycloak实例,让我们启动Jboss提供的Docker容器:docker run -d \ –name springboot-security-keycloak-integration \ -e KEYCLOAK_USER=admin \ -e KEYCLOAK_PASSWORD=admin \ -p 9001:8080 \ jboss/keycloak在此之后,我们只需登录到容器并导航到bin文件夹。docker exec -it springboot-security-keycloak-integration /bin/bashcd keycloak/bin首先,我们需要从CLI客户端登录keycloak服务器,之后我们不再需要身份验证:./kcadm.sh config credentials –server http://localhost:8080/auth –realm master –user admin –password admin配置realm首先,我们需要创建一个realm:./kcadm.sh create realms -s realm=springboot-security-keycloak-integration -s enabled=trueCreated new realm with id ‘springboot-security-keycloak-integration’之后,我们需要创建2个客户端,这将为我们的应用程序提供身份验证。首先我们创建一个cURL客户端,这样我们就可以通过命令行命令登录:./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=curl -s enabled=true -s publicClient=true -s baseUrl=http://localhost:8080 -s adminUrl=http://localhost:8080 -s directAccessGrantsEnabled=trueCreated new client with id ‘8f0481cd-3bbb-4659-850f-6088466a4d89’重要的是要注意2个选项:publicClient=true和 directAccessGrantsEnabled=true。第一个使这个客户端公开,这意味着我们的cURL客户端可以在不提供任何秘密的情况下启动登录。第二个使我们能够使用用户名和密码直接登录。其次,我们创建了一个由REST服务使用的客户端:./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=springboot-security-keycloak-integration-client -s enabled=true -s baseUrl=http://localhost:8080 -s bearerOnly=trueCreated new client with id ‘ab9d404e-6d5b-40ac-9bc3-9e2e26b68213’这里的重要配置是bearerOnly=true。这告诉Keycloak客户端永远不会启动登录过程,但是当它收到Bearer令牌时,它将检查所述令牌的有效性。我们应该注意保留这些ID,因为我们将在接下来的步骤中使用它们。我们有两个客户端,接下来是为spring-security-keycloak-example-app客户创建角色Admin Role:./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=admin -s ‘description=Admin role’Created new role with id ‘admin’User Role:./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=user -s ‘description=User role’Created new role with id ‘user’注意client后的id是我们创建客户端输出的id最后,我们应该获取客户端的配置,以便稍后提供给我们的应用程序:./kcadm.sh get clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/installation/providers/keycloak-oidc-keycloak-json -r springboot-security-keycloak-integration注意client后的id是我们创建客户端输出的id应该返回类似于此的内容:{ “realm” : “springboot-security-keycloak-integration”, “bearer-only” : true, “auth-server-url” : “http://localhost:8080/auth”, “ssl-required” : “external”, “resource” : “springboot-security-keycloak-integration-client”, “verify-token-audience” : true, “use-resource-role-mappings” : true, “confidential-port” : 0}配置用户出于演示目的,我们创建2个具有2个不同角色的用户,以便我们验证授权是否有效。首先,让我们创建一个具有admin角色的用户:创建admin用户:./kcadm.sh create users -r springboot-security-keycloak-integration -s username=admin -s enabled=trueCreated new user with id ‘50c11a76-a8ff-42b1-80cb-d82cb3e7616d’设置admin密码:./kcadm.sh update users/50c11a76-a8ff-42b1-80cb-d82cb3e7616d/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -nvalue: 用户密码追加到admin角色中./kcadm.sh add-roles -r springboot-security-keycloak-integration –uusername=admin –cclientid springboot-security-keycloak-integration-client –rolename admin注意:从不在生产中使用此方法,它仅用于演示目的!然后我们创建另一个用户,这次有角色user:创建user用户:./kcadm.sh create users -r springboot-security-keycloak-integration -s username=user -s enabled=trueCreated new user with id ‘624434c8-bce4-4b5b-b81f-e77304785803’设置user密码:./kcadm.sh update users/624434c8-bce4-4b5b-b81f-e77304785803/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -n追加到user角色中:./kcadm.sh add-roles -r springboot-security-keycloak-integration –uusername=user –cclientid springboot-security-keycloak-integration-client –rolename userRest服务我们已经配置了Keycloak并准备使用,我们只需要一个应用程序来使用它!所以我们创建一个简单的Spring Boot应用程序。我会在这里使用maven构建项目:<project xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://maven.apache.org/POM/4.0.0" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.edurt.sski</groupId> <artifactId>springboot-security-keycloak-integration</artifactId> <packaging>jar</packaging> <version>1.0.0</version> <name>springboot security keycloak integration</name> <description>SpringBoot Security KeyCloak Integration is a open source springboot, spring security, keycloak integration example. </description> <properties> <!– dependency config –> <dependency.lombox.version>1.16.16</dependency.lombox.version> <dependency.springboot.common.version>1.5.6.RELEASE</dependency.springboot.common.version> <dependency.keycloak.version>3.1.0.Final</dependency.keycloak.version> <!– plugin config –> <plugin.maven.compiler.version>3.3</plugin.maven.compiler.version> <plugin.maven.javadoc.version>2.10.4</plugin.maven.javadoc.version> <!– environment config –> <environment.compile.java.version>1.8</environment.compile.java.version> <!– reporting config –> <reporting.maven.jxr.version>2.5</reporting.maven.jxr.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${dependency.springboot.common.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!– lombok –> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${dependency.lombox.version}</version> </dependency> <!– springboot –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!– keycloak –> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-starter</artifactId> <version>${dependency.keycloak.version}</version> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-security-adapter</artifactId> <version>${dependency.keycloak.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${plugin.maven.compiler.version}</version> <configuration> <source>${environment.compile.java.version}</source> <target>${environment.compile.java.version}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <version>${plugin.maven.javadoc.version}</version> <configuration> <aggregate>true</aggregate> <!– custom tags –> <tags> <tag> <name>Description</name> <placement>test</placement> <head>description</head> </tag> </tags> <!– close jdoclint check document –> <additionalparam>-Xdoclint:none</additionalparam> </configuration> </plugin> </plugins> </build> <reporting> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jxr-plugin</artifactId> <version>${reporting.maven.jxr.version}</version> </plugin> </plugins> </reporting></project>添加所有必需的依赖项:spring-security 用于保护应用程序keycloak-spring-boot-starter 使用Keycloak和Spring Bootkeycloak-spring-security-adapter 与Spring Security集成一个简单的应用类:/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.edurt.sski;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/* * <p> SpringBootSecurityKeyCloakIntegration </p> * <p> Description : SpringBootSecurityKeyCloakIntegration </p> * <p> Author : qianmoQ </p> * <p> Version : 1.0 </p> * <p> Create Time : 2019-02-18 14:45 </p> * <p> Author Email: <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> </p> /@SpringBootApplicationpublic class SpringBootSecurityKeyCloakIntegration { public static void main(String[] args) { SpringApplication.run(SpringBootSecurityKeyCloakIntegration.class, args); }}Rest API接口:/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.edurt.sski.controller;import org.springframework.security.access.annotation.Secured;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;/* * <p> HelloController </p> * <p> Description : HelloController </p> * <p> Author : qianmoQ </p> * <p> Version : 1.0 </p> * <p> Create Time : 2019-02-18 14:50 </p> * <p> Author Email: <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> </p> /@RestControllerpublic class HelloController { @GetMapping(value = “/admin”) @Secured(“ROLE_ADMIN”) public String admin() { return “Admin”; } @GetMapping("/user”) @Secured(“ROLE_USER”) public String user() { return “User”; }}最后是keycloak配置:/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.edurt.sski.config;import org.keycloak.adapters.KeycloakConfigResolver;import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;/* * <p> KeycloakSecurityConfigurer </p> * <p> Description : KeycloakSecurityConfigurer </p> * <p> Author : qianmoQ </p> * <p> Version : 1.0 </p> * <p> Create Time : 2019-02-18 14:51 </p> * <p> Author Email: <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> </p> */@Configuration@EnableWebSecuritypublic class KeycloakSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter { @Bean public GrantedAuthoritiesMapper grantedAuthoritiesMapper() { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper; } @Override protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() { final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider(); provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper()); return provider; } @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(keycloakAuthenticationProvider()); } @Override protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new NullAuthenticatedSessionStrategy(); } @Override protected void configure(final HttpSecurity http) throws Exception { super.configure(http); http .authorizeRequests() .antMatchers("/admin”).hasRole(“ADMIN”) .antMatchers("/user”).hasRole(“USER”) .anyRequest().permitAll(); } @Bean KeycloakConfigResolver keycloakConfigResolver() { return new KeycloakSpringBootConfigResolver(); } @Bean public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( final KeycloakAuthenticationProcessingFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; } @Bean public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( final KeycloakPreAuthActionsFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; }}KeycloakSecurityConfigurer类扩展 KeycloakWebSecurityConfigurerAdapter,这是Keycloak提供的类,它提供与Spring Security的集成。然后我们通过添加SimpleAuthorityMapper配置身份验证管理器,它负责转换来自Keycloak的角色名称以匹配Spring Security的约定。基本上Spring Security期望以ROLE_前缀开头的角色,ROLE_ADMIN可以像Keycloak一样命名我们的角色,或者我们可以将它们命名为admin,然后使用此映射器将其转换为大写并添加必要的ROLE_前缀:@Beanpublic GrantedAuthoritiesMapper grantedAuthoritiesMapper() { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper;}@Overrideprotected KeycloakAuthenticationProvider keycloakAuthenticationProvider() { final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider(); provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper()); return provider;}@Overrideprotected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(keycloakAuthenticationProvider());}我们还需要为Keycloak设置会话策略,但是当我们创建无状态REST服务时,我们并不真的想要有会话,因此我们使用NullAuthenticatedSessionStrategy:@Overrideprotected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new NullAuthenticatedSessionStrategy();}通常,Keycloak Spring Security集成从keycloak.json文件中解析keycloak配置,但是我们希望有适当的Spring Boot配置,因此我们使用Spring Boot覆盖配置解析器:@BeanKeycloakConfigResolver keycloakConfigResolver() { return new KeycloakSpringBootConfigResolver();}然后我们配置Spring Security来授权所有请求:@Overrideprotected void configure(final HttpSecurity http) throws Exception { super.configure(http); http .authorizeRequests() .anyRequest().permitAll();}最后,根据文档,我们阻止双重注册Keycloak的过滤器:@Beanpublic FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( final KeycloakAuthenticationProcessingFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean;}@Beanpublic FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( final KeycloakPreAuthActionsFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean;}最后,我们需要application.properties使用之前下载的值配置我们的应用程序 :server.port=9002keycloak.realm=springboot-security-keycloak-integrationkeycloak.bearer-only=truekeycloak.auth-server-url=http://localhost:9001/authkeycloak.ssl-required=externalkeycloak.resource=springboot-security-keycloak-integration-clientkeycloak.use-resource-role-mappings=truekeycloak.principal-attribute=preferred_username使用应用程序使用curl我们创建的客户端进行身份验证,以获取访问令牌:export TOKEN=curl -ss --data "grant_type=password&amp;client_id=curl&amp;username=admin&amp;password=admin" http://localhost:9001/auth/realms/springboot-security-keycloak-integration/protocol/openid-connect/token | jq -r .access_token这将收到的访问令牌存储在TOKEN变量中。现在我们可以检查我们的管理员是否可以访问自己的/admin接口curl -H “Authorization: bearer $TOKEN” http://localhost:9002/adminAdmin但它无法访问/user接口:$ curl -H “Authorization: bearer $TOKEN” http://localhost:9002/user{“timestamp”:1498728302626,“status”:403,“error”:“Forbidden”,“message”:“Access is denied”,“path”:"/user"}对于user用户也是如此,user用户无法访问admin接口。源码地址:GitHub ...

February 18, 2019 · 6 min · jiezi

SpringBoot参数校验

本篇概述 在正常的项目开发中,我们常常需要对程序的参数进行校验来保证程序的安全性。参数校验非常简单,说白了就是对参数进行正确性验证,例如非空验证、范围验证、类型验证等等。校验的方式也有很多种。如果架构设计的比较好的话,可能我们都不需要做任何验证,或者写比较少的代码就可以满足验证的需求。如果架构设计的有缺陷,或者说压根就没有架构的话,那么我们对参数进行验证时,就需要我们写大量相对重复的代码进行验证了。手动参数校验 下面我们还是以上一篇的内容为例,我们首先手动对参数进行校验。下面为Controller源码:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(UserInfoQuery userInfo) { if (StringUtils.isEmpty(userInfo.getUsername())) { return “账号不能为空”; } if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) { return “权限不能为空,并且范围为[1-99]”; } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们只验证了username和roleId参数,分别验证为空验证及范围验证。下面我们测试一下。启动项目后,访问以下地址:http://127.0.0.1:8080/springb… 我们看一下程序的运行结果。 因为我们没有写任何参数,所以参数验证一定是不能通过的。所以就返回的上图中的提示信息。下面我们看一下数据库中的数据,然后访问一下正确的地址,看看能不能成功的返回数据库中的数据。下图为数据库中的数据: 下面我们访问一下正确的参数,然后看一下返回的结果。访问地址:http://127.0.0.1:8080/springb… 访问结果: 我们看上图已经成功的返回数据库中的数据了,这就是简单的参数校验,正是因为简单,所以我们就不做过多的介绍了。下面我们简单分析一下,这样做参数验证好不好。如果我们的项目比较简单,那答案一定是肯定的,因为站在软件设计角度考虑,没必要为了一个简单的功能而设计一个复杂的架构。因为越是复杂的功能,出问题的可能性就越大,程序就越不稳定。但如果站在程序开发角度,那上面的代码一定是有问题的,因为上面的代码根本没办法复用,如果要开发很多这样的项目,要进行参数验证时,那结果一定是代码中有很多相类似的代码,这显然是不合理的。那怎么办呢?那答案就是本篇中的重点内容,也就是SpringBoot对参数的验证,实际上本篇的内容主要是和Spring内容相关和SpringBoot的关系不大。但SpringBoot中基本包括了所有Spring的内容,所以我们还是以SpringBoot项目为例。下面我们看一下,怎么在SpringBoot中的对参数进行校验。ObjectError参数校验 我们首先看一下代码,然后在详细介绍代码中的新知识。下面为接受的参数类的源码。 修改前:package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;@Component@Datapublic class UserInfoQuery{ private String username; private Long roleId;} 修改后:package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;@Component@Datapublic class UserInfoQuery{ @NotNull(message = “账号不能为空”) private String username; @NotNull(message = “权限不能为空”) @Min(value = 1, message = “权限范围为[1-99]”) @Max(value = 99, message = “权限范围为[1-99]”) private Long roleId;} 我们看代码中唯一的区别就是添加了很多的注解。没错,在SpringBoot项目中进行参数校验时,就是使用这些注解来完成的。并且注解的命名很直观,基本上通过名字就可以知道什么含义。唯一需要注意的就是这些注解的包是javax中的,而不是其它第三方引入的包。这一点要特别注意,因为很多第三方的包,也包含这些同名的注解。下面我们继续看Controller中的改动(备注:有关javax中的校验注解相关的使用说明,我们后续在做介绍)。Controller源码: 改动前:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(UserInfoQuery userInfo) { if (StringUtils.isEmpty(userInfo.getUsername())) { return “账号不能为空”; } if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) { return “权限不能为空,并且范围为[1-99]”; } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 改动后:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.ObjectError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { for (ObjectError error : result.getAllErrors()) { return error.getDefaultMessage(); } } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们看代码改动的还是比较大的首先在入参中添加了@Valid注解。该注解就是标识让SpringBoot对请求参数进行验证。也就是和参数类里的注解是对应的。其次我们修改了直接在Controller中进行参数判断的逻辑,将以前的代码修改成了SpringBoot中指定的校验方式。下面我们启动项目,来验证一下上述代码是否能成功的验证参数的正确性。我们访问下面请求地址:http://127.0.0.1:8080/springb… 返回结果: 我们看上图成功的验证了为空的校验,下面我们试一下范围的验证。我们访问下面的请求地址:http://127.0.0.1:8080/springb… 看一下返回结果: 我们看成功的检测到了参数范围不正确。这就是SpringBoot中的参数验证功能。但上面的代码一个问题,就是只是会返回错误的提示信息,而没有提示,是哪个参数不正确。下面我们修改一下代码,来看一下怎么返回是哪个参数不正确。FieldError参数校验package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { FieldError error = result.getFieldError(); return error.getField() + “+” + error.getDefaultMessage(); } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们将获取ObjectError的类型修改成了FieldError。因为FieldError类型可以获取到验证错误的字段名字,所以我们将ObjectError修改为FieldError。下面我们看一下请求返回的结果。 我们看这回我们就获取到了验证错误的字段名子了。在实际的项目开发中,我们在返回接口数据时,大部分都会采用json格式的方式返回,下面我们简单封装一个返回的类,使上面的验证返回json格式。下面为封装的返回类的源码:package com.jilinwula.springboot.helloworld.utils;import lombok.Data;@Datapublic class Return { private int code; private Object data; private String msg; public static Return error(Object data, String msg) { Return r = new Return(); r.setCode(-1); r.setData(data); r.setMsg(msg); return r; }} Controller修改:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import com.jilinwula.springboot.helloworld.utils.Return;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { FieldError error = result.getFieldError(); return Return.error(error.getField(), error.getDefaultMessage()); } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们还是启动项目,并访问下面地址看看返回的结果:http://127.0.0.1:8080/springb… 返回结果: 创建切面 这样我们就返回一个简单的json类型的数据了。虽然我们的校验参数的逻辑没有在Controller里面写,但我们还是在Controller里面写了很多和业务无关的代码,并且这些代码还是重复的,这显然是不合理的。我们可以将上述相同的代码的封装起来,然后统一的处理。这样就避免了有很多重复的代码了。那这代码封装到哪里呢?我们可以使用Spring中的切面功能。因为SpringBoot中基本包括了所有Spring中的技术,所以,我们可以放心大胆的在SpringBoot项目中使用Spring中的技术。我们知道在使用切面技术时,我们可以对方法进行前置增强、后置增强、环绕增强等。这样我们就可以利用切面的技术,在方法之前,也就是请求Controller之前,做参数的校验工作,这样就不会对我们的业务代码产生侵入了。下面我们看一下切面的源码然后在做详细说明:package com.jilinwula.springboot.helloworld.aspect;import com.jilinwula.springboot.helloworld.utils.Return;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;@Slf4j@Aspect@Componentpublic class UserAspect { @Before(“execution(public * com.jilinwula.springboot.helloworld.controller..*(..))”) public void doBefore(JoinPoint joinPoint) { for (Object arg : joinPoint.getArgs()) { if (arg instanceof BindingResult) { BindingResult result = (BindingResult) arg; if (result.hasErrors()) { FieldError error = result.getFieldError(); Return.error(error.getField(), error.getDefaultMessage()); } } } }} 我们看上述的代码中我们添加了一个@Aspect注解,这个就是切面的注解,然后我们在方法中又添加了@Before注解,也就是对目标方法进行前置增强,Spring在请求Controller之前会先请求此方法。所以我们可以将校验参数的代码逻辑写在这个方法中。execution参数为切点函数,也就是目标方法的切入点。切点函数包含一些通配符的语法,下面我们简单介绍一下:匹配任意字符,但它可能匹配上下文中的一个元素.. 匹配任意字符,可以匹配上下文中的多个元素表示按类型匹配指定类的所有类,必须跟在类名后面,也就是会匹配继承或者扩展指定类的所有类,包括指定类.创建异常类 我们通过上述代码知道,Spring中的切面功能是没有返回值的。所以我们在使用切面功能时,是没有办法在切面里面做参数返回的。那我们应该怎么办呢?这时异常就派上用场了。我们知道当程序抛出异常时,如果当前方法没有做try catch处理,那么异常就会一直向上抛出,如果程序也一直没有做处理,那么当前异常就会一直抛出,直到被Java虚拟机捕获。但Java虚拟机也不会对异常进行处理,而是直接抛出异常。这也就是程序不做任何处理抛出异常的根本原因。我们正好可以利用异常的这种特性,返回参数验证的结果。因为在Spring中为我们提供了统一捕获异常的方法,我们可以在这个方法中,将我们的异常信息封装成json格式,这样我们就可以返回统一的jons格式了。所以在上述的切面中我们手动了抛出了一个异常。该异常因为我们没有用任何处理,所以上述异常会被SpringBoot中的统一异常拦截处理。这样当SpringBoot检测到参数不正确时,就会抛出一个异常,然后SpringBoot就会检测到程序抛出的异常,然后返回异常中的信息。下面我们看一下异常类的源码: 异常类:package com.jilinwula.springboot.helloworld.exception;import com.jilinwula.springboot.helloworld.utils.Return;import lombok.Data;@Datapublic class UserInfoException extends RuntimeException { private Return r; public UserInfoException(Return r) { this.r = r; }} Return源码:package com.jilinwula.springboot.helloworld.utils;import com.jilinwula.springboot.helloworld.exception.UserInfoException;import lombok.Data;@Datapublic class Return { private int code; private Object data; private String msg; public static void error(Object data, String msg) { Return r = new Return(); r.setCode(-1); r.setData(data); r.setMsg(msg); throw new UserInfoException(r); } public static Return success() { Return r = new Return(); r.setCode(0); return r; }}SpringBoot统一异常拦截 因为该异常类比较简单,我们就不会过多的介绍了,唯一有一点需要注意的是该异常类继承的是RuntimeException异常类,而不是Exception异常类,原因我们已经在上一篇中介绍了,Spring只会回滚RuntimeException异常类及其子类,而不会回滚Exception异常类的。下面我们看一下Spring中统一拦截异常处理,下面为该类的源码:package com.jilinwula.springboot.helloworld.handler;import com.jilinwula.springboot.helloworld.exception.UserInfoException;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvicepublic class UserInfoHandler { /** * 校验错误拦截处理 * * @param e 错误信息集合 * @return 错误信息 */ @ExceptionHandler(UserInfoException.class) public Object handle(UserInfoException e) { return e.getR(); }} 我们在该类添加了@RestControllerAdvice注解。该注解就是为了定义我们统一获取异常拦截的。然后我们又添加了@ExceptionHandler注解,该注解就是用来拦截异常类的注解,并且可以在当前方法中,直接获取到该异常类的对象信息。这样我们直接返回这个异常类的信息就可以了。因为我们在这个自定义异常类中添加了Return参数,所以,我们只要反悔Return对象的信息即可,而不用返回整个异常的信息。下面我们访问一下下面的请求,看看上述代码是否能检测到参数不正确。请求地址:http://127.0.0.1:8080/springb… 返回结果: 这样我们完成了参数校验的功能了,并且这种方式有很大的复用性,即使我们在写新的Controller,也不需要手动的校验参数了,只要我们的请求参数是UserInfoQuery类就可以了。还有一点要注意,所以我们不用手动验证参数了,但我们的请求参数中还是要写BindingResult参数,这一点要特别注意。正则表达式校验注解 下面我们更详细的介绍一下参数验证的注解,我们首先看一下正则校验,我们在实体类中添加一个新属性,然后用正则的的方式,验证该参数的正确性。下面为实体类源码:package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;import javax.validation.constraints.Pattern;@Component@Datapublic class UserInfoQuery{ @NotNull(message = “用户编号不能为空”) @Pattern(regexp = “^[1-10]$",message = “用户编号范围不正确”) private String id; @NotNull(message = “账号不能为空”) private String username; @NotNull(message = “权限不能为空”) @Min(value = 1, message = “权限范围为[1-99]”) @Max(value = 99, message = “权限范围为[1-99]”) private Long roleId;} 下面我们访问以下地址:http://127.0.0.1:8080/springb…http文件请求接口 但这回我们不在浏览器里请求,因为浏览器请求不太方便,并且返回的json格式也没有格式化不方便浏览,除非要装一些浏览器插件才可以。实际上在IDEA中我们可以很方便的请求一下接口地址,并且返回的json内容是自动格式化的。下面我们来看一下怎么在IDEA中发起接口请求。在IDEA中请求一个接口很简单,我们只要创建一个.http类型的文件名字就可以。然后我们可以在该文件中,指定我们接口的请求类型,例如GET或者POST。当我们在文件的开口写GET或者POST时,IDEA会自动有相应的提示。下面我们看一下http文件中的内容。 http.http:GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=-1 这时标识GET参数的地方,就会出现绿色剪头,但我们点击这个绿色箭头,IDEA就会就会启动请求GET参数后面的接口。下面我们看一下上述的返回结果。GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=-1HTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 18 Feb 2019 03:57:29 GMT{ “code”: -1, “data”: “id”, “msg”: “用户编号范围不正确”}Response code: 200; Time: 24ms; Content length: 41 bytes 这就是.http文件类型的返回结果,用该文件请求接口,相比用浏览器来说,要方便的多。因为我们在实体类中使用正则指定参数范围为1-10,所以请求接口时反悔了id参数有错误。下面我们输入一个正确的值在看一下返回结果。 http.http:GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=1 返回结果: GET <http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=1> HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Mon, 18 Feb 2019 05:46:49 GMT { “id”: 61, “username”: “阿里巴巴”, “password”: “alibaba”, “nickname”: “阿里巴巴”, “roleId”: 3 } Response code: 200; Time: 25ms; Content length: 77 bytes常见校验注解 我们看已经正确的返回数据库中的数据了。在Spring中,提供了很多种注解来方便我们进行参数校验,下面是比较常见的注解:注解作用@Null参数必须为null @NotNull参数必须不为null @NotBlank参数必须不为null,并且长度必须大于0 @NotEmpty参数必须不为空 @Min参数必须大于等于该值 @Max参数必须小于等于该值 @Size参数必须在指定的范围内 @Past参数必须是一个过期的时间 @Future参数必须是一个未来的时间 @Pattern参数必须满足正则表达式 @Email参数必须为电子邮箱 上述内容就是SpringBoot中的参数校验全部内容,如有不正确的欢迎留言,谢谢。源码地址https://github.com/jilinwula/…原文地址http://jilinwula.com/article/… ...

February 18, 2019 · 3 min · jiezi

Spring Security OAuth 个性化token

个性化Token 目的默认通过调用 /oauth/token 返回的报文格式包含以下参数{ “access_token”: “e6669cdf-b6cd-43fe-af5c-f91a65041382”, “token_type”: “bearer”, “refresh_token”: “da91294d-446c-4a89-bdcf-88aee15a75e8”, “expires_in”: 43199, “scope”: “server”}并没包含用户的业务信息比如用户信息、租户信息等。扩展生成包含业务信息(如下),避免系统多次调用,直接可以通过认证接口获取到用户信息等,大大提高系统性能{ “access_token”:“a6f3b6d6-93e6-4eb8-a97d-3ae72240a7b0”, “token_type”:“bearer”, “refresh_token”:“710ab162-a482-41cd-8bad-26456af38e4f”, “expires_in”:42396, “scope”:“server”, “tenant_id”:1, “license”:“made by pigx”, “dept_id”:1, “user_id”:1, “username”:“admin”}密码模式生成Token 源码解析 主页参考红框部分ResourceOwnerPasswordTokenGranter (密码模式)根据用户的请求信息,进行认证得到当前用户上下文信息protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters()); String username = parameters.get(“username”); String password = parameters.get(“password”); // Protect from downstream leaks of password parameters.remove(“password”); Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); ((AbstractAuthenticationToken) userAuth).setDetails(parameters); userAuth = authenticationManager.authenticate(userAuth); OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest); return new OAuth2Authentication(storedOAuth2Request, userAuth);}然后调用AbstractTokenGranter.getAccessToken() 获取OAuth2AccessTokenprotected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));}默认使用DefaultTokenServices来获取tokenpublic OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { … 一系列判断 ,合法性、是否过期等判断 OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); tokenStore.storeAccessToken(accessToken, authentication); // In case it was modified refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken;}createAccessToken 核心逻辑// 默认刷新token 的有效期private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.// 默认token 的有效期private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(uuid); token.setExpiration(Date) token.setRefreshToken(refreshToken); token.setScope(authentication.getOAuth2Request().getScope()); return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;}如上代码,在拼装好token对象后会调用认证服务器配置TokenEnhancer( 增强器) 来对默认的token进行增强。TokenEnhancer.enhance 通过上下文中的用户信息来个性化Tokenpublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 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;}基于pig 看下最终的实现效果Pig 基于Spring Cloud、oAuth2.0开发基于Vue前后分离的开发平台,支持账号、短信、SSO等多种登录,提供配套视频开发教程。 https://gitee.com/log4j/pig ...

February 18, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App(五)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章主要讲的是聊天App_PigChat中关于聊天功能的实现。移除方法与处理异常方法的重写在ChatHandler中重写其移除channel的方法handlerRemoved,以及处理异常的方法exceptionCaught。 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { String channelId = ctx.channel().id().asShortText(); System.out.println(“客户端被移除,channelId为:” + channelId); // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel users.remove(ctx.channel()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除 ctx.channel().close(); users.remove(ctx.channel()); }定义消息的实体类public class ChatMsg implements Serializable { private static final long serialVersionUID = 3611169682695799175L; private String senderId; // 发送者的用户id private String receiverId; // 接受者的用户id private String msg; // 聊天内容 private String msgId; // 用于消息的签收 public String getSenderId() { return senderId; } public void setSenderId(String senderId) { this.senderId = senderId; } public String getReceiverId() { return receiverId; } public void setReceiverId(String receiverId) { this.receiverId = receiverId; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getMsgId() { return msgId; } public void setMsgId(String msgId) { this.msgId = msgId; } }对实体类再做一层包装public class DataContent implements Serializable { private static final long serialVersionUID = 8021381444738260454L; private Integer action; // 动作类型 private ChatMsg chatMsg; // 用户的聊天内容entity private String extand; // 扩展字段 public Integer getAction() { return action; } public void setAction(Integer action) { this.action = action; } public ChatMsg getChatMsg() { return chatMsg; } public void setChatMsg(ChatMsg chatMsg) { this.chatMsg = chatMsg; } public String getExtand() { return extand; } public void setExtand(String extand) { this.extand = extand; }}定义发送消息的动作的枚举类型public enum MsgActionEnum { CONNECT(1, “第一次(或重连)初始化连接”), CHAT(2, “聊天消息”), SIGNED(3, “消息签收”), KEEPALIVE(4, “客户端保持心跳”), PULL_FRIEND(5, “拉取好友”); public final Integer type; public final String content; MsgActionEnum(Integer type, String content){ this.type = type; this.content = content; } public Integer getType() { return type; } }定义记录用户与channel关系的类/** * @Description: 用户id和channel的关联关系处理 /public class UserChannelRel { private static HashMap<String, Channel> manager = new HashMap<>(); public static void put(String senderId, Channel channel) { manager.put(senderId, channel); } public static Channel get(String senderId) { return manager.get(senderId); } public static void output() { for (HashMap.Entry<String, Channel> entry : manager.entrySet()) { System.out.println(“UserId: " + entry.getKey() + “, ChannelId: " + entry.getValue().id().asLongText()); } }}接受与处理消息方法的重写重写ChatHandler读取消息的channelRead0方法。具体步骤如下:(1)获取客户端发来的消息;(2)判断消息类型,根据不同的类型来处理不同的业务;(2.1)当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来;(2.2)聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收];然后实现消息的发送,首先从全局用户Channel关系中获取接受方的channel,然后当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在,若用户在线,则使用writeAndFlush方法向其发送消息;(2.3)签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收];(2.4)心跳类型的消息 // 用于记录和管理所有客户端的channle public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println(“read……….”); // 获取客户端传输过来的消息 String content = msg.text(); Channel currentChannel = ctx.channel(); // 1. 获取客户端发来的消息 DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class); Integer action = dataContent.getAction(); // 2. 判断消息类型,根据不同的类型来处理不同的业务 if (action == MsgActionEnum.CONNECT.type) { // 2.1 当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来 String senderId = dataContent.getChatMsg().getSenderId(); UserChannelRel.put(senderId, currentChannel); // 测试 for (Channel c : users) { System.out.println(c.id().asLongText()); } UserChannelRel.output(); } else if (action == MsgActionEnum.CHAT.type) { // 2.2 聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收] ChatMsg chatMsg = dataContent.getChatMsg(); String msgText = chatMsg.getMsg(); String receiverId = chatMsg.getReceiverId(); String senderId = chatMsg.getSenderId(); // 保存消息到数据库,并且标记为 未签收 UserService userService = (UserService)SpringUtil.getBean(“userServiceImpl”); String msgId = userService.saveMsg(chatMsg); chatMsg.setMsgId(msgId); DataContent dataContentMsg = new DataContent(); dataContentMsg.setChatMsg(chatMsg); // 发送消息 // 从全局用户Channel关系中获取接受方的channel Channel receiverChannel = UserChannelRel.get(receiverId); if (receiverChannel == null) { // TODO channel为空代表用户离线,推送消息(JPush,个推,小米推送) } else { // 当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在 Channel findChannel = users.find(receiverChannel.id()); if (findChannel != null) { // 用户在线 receiverChannel.writeAndFlush( new TextWebSocketFrame( JsonUtils.objectToJson(dataContentMsg))); } else { // 用户离线 TODO 推送消息 } } } else if (action == MsgActionEnum.SIGNED.type) { // 2.3 签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收] UserService userService = (UserService)SpringUtil.getBean(“userServiceImpl”); // 扩展字段在signed类型的消息中,代表需要去签收的消息id,逗号间隔 String msgIdsStr = dataContent.getExtand(); String msgIds[] = msgIdsStr.split(”,”); List<String> msgIdList = new ArrayList<>(); for (String mid : msgIds) { if (StringUtils.isNotBlank(mid)) { msgIdList.add(mid); } } System.out.println(msgIdList.toString()); if (msgIdList != null && !msgIdList.isEmpty() && msgIdList.size() > 0) { // 批量签收 userService.updateMsgSigned(msgIdList); } } else if (action == MsgActionEnum.KEEPALIVE.type) { // 2.4 心跳类型的消息 System.out.println(“收到来自channel为[” + currentChannel + “]的心跳包…”); } } 获取未签收的消息列表的接口在controller中添加获取未签收的消息列表的接口getUnReadMsgList。 /* * * @Description: 用户手机端获取未签收的消息列表 */ @PostMapping("/getUnReadMsgList") public IMoocJSONResult getUnReadMsgList(String acceptUserId) { // 0. userId 判断不能为空 if (StringUtils.isBlank(acceptUserId)) { return IMoocJSONResult.errorMsg(""); } // 查询列表 List<com.imooc.pojo.ChatMsg> unreadMsgList = userService.getUnReadMsgList(acceptUserId); return IMoocJSONResult.ok(unreadMsgList); }测试 ...

February 18, 2019 · 3 min · jiezi

关于微服务架构的思考

最近在项目中遇到了一些问题,一个比较多的问题服务和服务直接调用混乱 a服务调用b b服务调用c c服务调用d 导致后期升级会出现很多问题 如果有个流程图也许会好些 但是没有 因此我陷入了思考, 如果进行重构的话那什么样的架构会是较好的价格 我想 设计模式的六大原则 在此也一样适用什么是好的架构明确的分工,服务之间优雅的调用我给出的一个结果这里简单画的一个草图先介绍一下查询:对应查询操作操作:对应增删改操作分为四层 ui: 页面及后台调用网关层: 路由聚合层:查询聚合 操作聚合服务层:订单服务 商品服务遵循的原则各个服务只专注于自己的功能 由聚合层来协调服务之间的关系维护与调用上层通过http调用下层 下层通过mq通知上层 同级不能调用服务要想调用服务 如 a服务想调用b服务 可以 a通过mq传递给聚合层 然后聚合层根据消息调用b ,服务之前的调用交给 聚合层维护后面还会不断完善这篇文章的

February 17, 2019 · 1 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(四)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章内容(1) 查询好友列表的接口(2)通过或忽略好友请求的接口(3)添加好友功能展示查询好友列表的接口 /** * @Description: 查询我的好友列表 / @PostMapping("/myFriends") public IMoocJSONResult myFriends(String userId) { // 0. userId 判断不能为空 if (StringUtils.isBlank(userId)) { return IMoocJSONResult.errorMsg(""); } // 1. 数据库查询好友列表 List<MyFriendsVO> myFirends = userService.queryMyFriends(userId); return IMoocJSONResult.ok(myFirends); }通过或忽略好友请求的接口定义枚举类型/* * * @Description: 忽略或者通过 好友请求的枚举 /public enum OperatorFriendRequestTypeEnum { IGNORE(0, “忽略”), PASS(1, “通过”); public final Integer type; public final String msg; OperatorFriendRequestTypeEnum(Integer type, String msg){ this.type = type; this.msg = msg; } public Integer getType() { return type; } public static String getMsgByType(Integer type) { for (OperatorFriendRequestTypeEnum operType : OperatorFriendRequestTypeEnum.values()) { if (operType.getType() == type) { return operType.msg; } } return null; } }controller中提供通过或忽略好友请求的接口 /* * @Description: 接受方 通过或者忽略朋友请求 */ @PostMapping("/operFriendRequest") public IMoocJSONResult operFriendRequest(String acceptUserId, String sendUserId, Integer operType) { // 0. acceptUserId sendUserId operType 判断不能为空 if (StringUtils.isBlank(acceptUserId) || StringUtils.isBlank(sendUserId) || operType == null) { return IMoocJSONResult.errorMsg(""); } // 1. 如果operType 没有对应的枚举值,则直接抛出空错误信息 if (StringUtils.isBlank(OperatorFriendRequestTypeEnum.getMsgByType(operType))) { return IMoocJSONResult.errorMsg(""); } if (operType == OperatorFriendRequestTypeEnum.IGNORE.type) { // 2. 判断如果忽略好友请求,则直接删除好友请求的数据库表记录 userService.deleteFriendRequest(sendUserId, acceptUserId); } else if (operType == OperatorFriendRequestTypeEnum.PASS.type) { // 3. 判断如果是通过好友请求,则互相增加好友记录到数据库对应的表 // 然后删除好友请求的数据库表记录 userService.passFriendRequest(sendUserId, acceptUserId); } // 4. 数据库查询好友列表 List<MyFriendsVO> myFirends = userService.queryMyFriends(acceptUserId); // 5. 将查询到的好友列表返回给前端 return IMoocJSONResult.ok(myFirends); }添加好友功展示通过搜索好友姓名添加好友通过扫描二维码添加好友 ...

February 17, 2019 · 1 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(三)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本节主要讲解聊天App PigChat中关于好友申请的发送与接受。包含以下内容:(1)搜索好友接口(2)发送添加好友申请的接口(3)接受添加好友申请的接口搜索好友接口定义枚举类型 SearchFriendsStatusEnum,表示添加好友的前置状态 SUCCESS(0, “OK”), USER_NOT_EXIST(1, “无此用户…”), NOT_YOURSELF(2, “不能添加你自己…”), ALREADY_FRIENDS(3, “该用户已经是你的好友…”);在service中定义搜索朋友的前置条件判断的方法preconditionSearchFriends。传入的是用户的Id以及搜索的用户的名称。【1】首先根据搜索的用户的名称查找是否存在这个用户。【2】如果搜索的用户不存在,则返回[无此用户]。【3】如果搜索的用户是你自己,则返回[不能添加自己]。【4】如果搜索的用户已经是你的好友,则返回[该用户已经是你的好友]。【5】否则返回成功。 @Transactional(propagation = Propagation.SUPPORTS) @Override public Integer preconditionSearchFriends(String myUserId, String friendUsername) { //1. 查找要添加的朋友是否存在 Users user = queryUserInfoByUsername(friendUsername); // 2. 搜索的用户如果不存在,返回[无此用户] if (user == null) { return SearchFriendsStatusEnum.USER_NOT_EXIST.status; } // 3. 搜索账号是你自己,返回[不能添加自己] if (user.getId().equals(myUserId)) { return SearchFriendsStatusEnum.NOT_YOURSELF.status; } // 4. 搜索的朋友已经是你的好友,返回[该用户已经是你的好友] Example mfe = new Example(MyFriends.class); Criteria mfc = mfe.createCriteria(); mfc.andEqualTo(“myUserId”, myUserId); mfc.andEqualTo(“myFriendUserId”, user.getId()); MyFriends myFriendsRel = myFriendsMapper.selectOneByExample(mfe); if (myFriendsRel != null) { return SearchFriendsStatusEnum.ALREADY_FRIENDS.status; } //返回成功 return SearchFriendsStatusEnum.SUCCESS.status; }在controller中创建搜索好友接口 searchUser。传入的是用户的Id,以及要搜索的用户的名字。【0】首先判断传入的参数是否为空。【1】通过userService的preconditionSearchFriends方法得到前置条件。【2】如果搜索前置条件为成功,则向前端返回搜索用户的信息。【3】否则搜索失败。 /** * @Description: 搜索好友接口, 根据账号做匹配查询而不是模糊查询 / @PostMapping("/search") public IMoocJSONResult searchUser(String myUserId, String friendUsername) throws Exception { // 0. 判断 myUserId friendUsername 不能为空 if (StringUtils.isBlank(myUserId) || StringUtils.isBlank(friendUsername)) { return IMoocJSONResult.errorMsg(""); } // 前置条件 - 1. 搜索的用户如果不存在,返回[无此用户] // 前置条件 - 2. 搜索账号是你自己,返回[不能添加自己] // 前置条件 - 3. 搜索的朋友已经是你的好友,返回[该用户已经是你的好友] //1. 得到前置条件状态 Integer status = userService.preconditionSearchFriends(myUserId, friendUsername); //2. 搜索成功,返回搜索用户的信息 if (status == SearchFriendsStatusEnum.SUCCESS.status) { Users user = userService.queryUserInfoByUsername(friendUsername); UsersVO userVO = new UsersVO(); BeanUtils.copyProperties(user, userVO); return IMoocJSONResult.ok(userVO); } else { //3. 搜索失败 String errorMsg = SearchFriendsStatusEnum.getMsgByKey(status); return IMoocJSONResult.errorMsg(errorMsg); } }发送添加好友申请的接口在service中定义添加好友请求记录,保存到数据库的sendFriendRequest方法。传入的是添加好友记录的发送方——用户的Id,以及记录的接收方——想要添加的朋友的名称。【1】首先根据用户名把朋友信息查询出来。【2】然后查询发送好友请求记录表。【3】如果不是你的好友,并且好友记录没有添加,则新增好友请求记录。这样可以保证打你发送了两次请求之后,数据库中仍然只记录一次请求的数据。 @Transactional(propagation = Propagation.REQUIRED) @Override public void sendFriendRequest(String myUserId, String friendUsername) { // 1. 根据用户名把朋友信息查询出来 Users friend = queryUserInfoByUsername(friendUsername); // 2. 查询发送好友请求记录表 Example fre = new Example(FriendsRequest.class); Criteria frc = fre.createCriteria(); frc.andEqualTo(“sendUserId”, myUserId); frc.andEqualTo(“acceptUserId”, friend.getId()); FriendsRequest friendRequest = friendsRequestMapper.selectOneByExample(fre); if (friendRequest == null) { // 3. 如果不是你的好友,并且好友记录没有添加,则新增好友请求记录 String requestId = sid.nextShort(); FriendsRequest request = new FriendsRequest(); request.setId(requestId); request.setSendUserId(myUserId); request.setAcceptUserId(friend.getId()); request.setRequestDateTime(new Date()); friendsRequestMapper.insert(request); } }在controller中创建发送添加好友请求的接口。传入的是添加好友记录的发送方——用户的Id,以及记录的接收方——想要添加的朋友的名称。【0】首先判断传入参数不为空。【1】然后判断前置条件,若为成功则通过userService的sendFriendRequest方法发送好友请求,否则返回失败。 /* * @Description: 发送添加好友的请求 / @PostMapping("/addFriendRequest") public IMoocJSONResult addFriendRequest(String myUserId, String friendUsername) throws Exception { // 0. 判断 myUserId friendUsername 不能为空 if (StringUtils.isBlank(myUserId) || StringUtils.isBlank(friendUsername)) { return IMoocJSONResult.errorMsg(""); } // 前置条件 - 1. 搜索的用户如果不存在,返回[无此用户] // 前置条件 - 2. 搜索账号是你自己,返回[不能添加自己] // 前置条件 - 3. 搜索的朋友已经是你的好友,返回[该用户已经是你的好友] // 1. 判断前置条件 Integer status = userService.preconditionSearchFriends(myUserId, friendUsername); if (status == SearchFriendsStatusEnum.SUCCESS.status) { userService.sendFriendRequest(myUserId, friendUsername); } else { String errorMsg = SearchFriendsStatusEnum.getMsgByKey(status); return IMoocJSONResult.errorMsg(errorMsg); } return IMoocJSONResult.ok(); }最终实现效果接受添加好友申请的接口在service中定义查询好友请求列表的queryFriendRequestList方法。 @Transactional(propagation = Propagation.SUPPORTS) @Override public List<FriendRequestVO> queryFriendRequestList(String acceptUserId) { return usersMapperCustom.queryFriendRequestList(acceptUserId); }在controller中定义接受添加好友请求的接口queryFriendRequests。 /* * @Description: 发送添加好友的请求 */ @PostMapping("/queryFriendRequests") public IMoocJSONResult queryFriendRequests(String userId) { // 0. 判断不能为空 if (StringUtils.isBlank(userId)) { return IMoocJSONResult.errorMsg(""); } // 1. 查询用户接受到的朋友申请 return IMoocJSONResult.ok(userService.queryFriendRequestList(userId)); } 最终实现效果 ...

February 16, 2019 · 2 min · jiezi

Spring Boot项目利用MyBatis Generator进行数据层代码自动生成

概 述MyBatis Generator (简称 MBG) 是一个用于 MyBatis和 iBATIS的代码生成器。它可以为 MyBatis的所有版本以及 2.2.0之后的 iBATIS版本自动生成 ORM层代码,典型地包括我们日常需要手写的 POJO、mapper xml 以及 mapper 接口等。MyBatis Generator 自动生成的 ORM层代码几乎可以应对大部分 CRUD 数据表操作场景,可谓是一个生产力工具啊!注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站数据库准备与工程搭建首先我们准备一个 MySQL数据表 user_info用于下文实验里面插入了若干条数据:新建一个Spring Boot 工程引入 MyBatis Generator 依赖<dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> <scope>provided</scope></dependency>引入 MyBatis Generator Maven 插件<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile> <overwrite>true</overwrite> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.12</version> </dependency> </dependencies></plugin>MyBatis Generator Maven 插件引入以后,我们可以在 Spring Boot工程的 Maven插件工具栏中看到新增的插件选项,类似下图:准备 MyBatis Generator 配置文件MyBatis Generator 也需要一个 xml格式的配置文件,该文件的位置配在了上文 引入 MyBatis Generator Maven 插件的 xml配置里,即src/main/resources/mybatis-generator.xml,下面给出本文所使用的配置:<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <context id=“MySql” defaultModelType=“flat”> <plugin type=“org.mybatis.generator.plugins.SerializablePlugin” /> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://121.196.123.245:3306/demo” userId=“root” password=“xxxxxx” /> <javaModelGenerator targetPackage=“cn.codesheep.springbt_mybatis_generator.entity” targetProject=“src/main/java”></javaModelGenerator> <sqlMapGenerator targetPackage=“mapper” targetProject=“src/main/resources”></sqlMapGenerator> <javaClientGenerator targetPackage=“cn.codesheep.springbt_mybatis_generator.mapper” targetProject=“src/main/java” type=“XMLMAPPER”></javaClientGenerator> <table tableName=“user_info”> <property name=“modelOnly” value=“false”/> </table> </context></generatorConfiguration>上面 xml中几个关键的配置简介如下:< jdbcConnection /> 数据库连接配置,至关重要<javaModelGenerator /> 指定自动生成的 POJO置于哪个包下<sqlMapGenerator /> 指定自动生成的 mapper.xml置于哪个包下<javaClientGenerator /> 指定自动生成的 DAO接口置于哪个包下<table /> 指定数据表名,可以使用_和%通配符更多关于 MyBatis Generator 配置的内容,可以移步 官方文档。运行 MyBatis Generator直接通过 IDEA的 Maven图形化插件来运行 MyBatis Generator,其自动生成的过程 和 生成的结果如下图所示:很明显,通过 MyBatis Generator,已经很方便的帮我们自动生成了 POJO、mapper xml 以及 mapper接口,接下来我们看看自动生成的代码怎么用!自动生成的代码如何使用我们发现通过 MyBatis Generator自动生成的代码中带有一个 Example文件,比如上文中的 UserInfoExample,其实 Example文件对于平时快速开发还是有很大好处的,它能节省很多写 sql语句的时间,举几个实际的例子吧:单条件模糊搜索 + 排序在我们的例子中,假如我想通过用户名 user_name来在 MySQL数据表 user_info中进行模糊搜索,并对结果进行排序,此时利用UserInfoExample可以方便快速的实现:@Autowiredprivate UserInfoMapper userInfoMapper;public List<UserInfo> searchUserByUserName( String userName ) { UserInfoExample userInfoExample = new UserInfoExample(); userInfoExample.createCriteria().andUserNameLike( ‘%’+ userName +’%’ ); // 设置模糊搜索的条件 String orderByClause = “user_name DESC”; userInfoExample.setOrderByClause( orderByClause ); // 设置通过某个字段排序的条件 return userInfoMapper.selectByExample( userInfoExample );}多条件精确搜索再比如,我们想通过电话号码 phone和用户名 user_name 两个字段来在数据表 user_info中实现精确搜索,则可以如下实现:public List<UserInfo> multiConditionsSearch( UserInfo userInfo ) { UserInfoExample userInfoExample = new UserInfoExample(); UserInfoExample.Criteria criteria = userInfoExample.createCriteria(); if( !”".equals(userInfo.getPhone()) ) criteria.andPhoneEqualTo( userInfo.getPhone() ); if( !"".equals(userInfo.getUserName()) ) criteria.andUserNameEqualTo( userInfo.getUserName() ); return userInfoMapper.selectByExample( userInfoExample );}很明显可以看出的是,我们是通过直接编写代码逻辑来取代编写 SQL语句,因此还是十分直观和容易理解的!后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

February 14, 2019 · 2 min · jiezi

最渣的 Spring Boot 文章

spring-boot-starter-parentMaven的用户可以通过继承spring-boot-starter-parent项目来获得一些合理的默认配置。这个parent提供了以下特性:默认使用Java 8使用UTF-8编码一个引用管理的功能,在dependencies里的部分配置可以不用填写version信息,这些version信息会从spring-boot-dependencies里得到继承。识别过来资源过滤(Sensible resource filtering.)识别插件的配置(Sensible plugin configuration (exec plugin, surefire, Git commit ID, shade).)能够识别application.properties和application.yml类型的文件,同时也能支持profile-specific类型的文件(如: application-foo.properties and application-foo.yml,这个功能可以更好的配置不同生产环境下的配置文件)。maven把默认的占位符${…}改为了@..@(这点大家还是看下原文自己理解下吧,我个人用的也比较少 since the default config files accept Spring style placeholders (${…}) the Maven filtering is changed to use @..@ placeholders (you can override that with a Maven property resource.delimiter).)starter启动器包含一些相应的依赖项, 以及自动配置等.Auto-configurationSpring Boot 支持基于Java的配置, 尽管可以将 SpringApplication 与 xml 一起使用, 但是还是建议使用 @Configuration.可以通过 @Import 注解导入其他配置类, 也可以通过 @ImportResource 注解加载XML配置文件.Spring Boot 自动配置尝试根据您添加的jar依赖项自动配置Spring应用程序. 例如, 如果项目中引入 HSQLDB jar, 并且没有手动配置任何数据库连接的bean, 则Spring Boot会自动配置内存数据库.您需要将 @EnableAutoConfiguration 或 @SpringBootApplication 其中一个注解添加到您的 @Configuration 类中, 从而选择进入自动配置.禁用自动配置import org.springframework.boot.autoconfigure.;import org.springframework.boot.autoconfigure.jdbc.;import org.springframework.context.annotation.*;@Configuration@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})public class MyConfiguration {}如果该类不在classpath中, 你可以使用该注解的excludeName属性, 并指定全限定名来达到相同效果. 最后, 你可以通过 spring.autoconfigure.exclude 属性 exclude 多个自动配置项(一个自动配置项集合)@ComponentScanSpringBoot在写启动类的时候如果不使用 @ComponentScan 指明对象扫描范围, 默认指扫描当前启动类所在的包里的对象.@SpringBootApplication@Target(value=TYPE) @Retention(value=RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters={@ComponentScan.Filter(type=CUSTOM,classes=TypeExcludeFilter.class),})public @interface SpringBootApplication使用 @SpringBootApplication 注解相当于使用了下面三个注解.@EnableAutoConfiguration: 启用 Spring Boot 的自动配置.@ComponentScan: 对应用程序所在的包启用 @Component 扫描.@Configuration: 允许在上下文中注册额外的bean或导入其他配置类.ApplicationRunner or CommandLineRunner 区别应用服务启动时,加载一些数据和执行一些应用的初始化动作。如:删除临时文件,清除缓存信息,读取配置文件信息,数据库连接等。1、SpringBoot提供了CommandLineRunner接口。当有该接口多个实现类时,提供了@order注解实现自定义执行顺序,也可以实现Ordered接口来自定义顺序。注意:数字越小,优先级越高,也就是@Order(1)注解的类会在@Order(2)注解的类之前执行。import com.example.studySpringBoot.service.MyMethorClassService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Component@Order(value=1)public class SpringDataInit implements CommandLineRunner { @Autowired private MyMethorClassService myMethorClassService; @Override public void run(String… strings) throws Exception { int result = myMethorClassService.add(8, 56); System.out.println("———-SpringDataInit1———"+result); }}2、SpringBoot提供的ApplicationRunner接口也可以满足该业务场景。不同点:ApplicationRunner中run方法的参数为ApplicationArguments,而CommandLineRunner接口中run方法的参数为String数组。想要更详细地获取命令行参数,那就使用ApplicationRunner接口import com.example.studySpringBoot.service.MyMethorClassService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;@Componentpublic class SpringDataInit3 implements ApplicationRunner,Ordered { @Autowired private MyMethorClassService myMethorClassService; @Override public void run(ApplicationArguments applicationArguments) throws Exception { int result = myMethorClassService.add(10, 82); System.out.println("———-SpringDataInit3———"+result); } @Override public int getOrder() { return 3; }}外部化配置Spring Boot允许你外部化你的配置,这样你就可以在不同的环境中使用相同的应用程序代码,你可以使用 properties 文件、YAML文件、环境变量和命令行参数来外部化配置,属性值可以通过使用 @Value 注解直接注入到你的bean中,通过Spring的 Environment 抽象访问,或者通过 @ConfigurationProperties 绑定到结构化对象。@ConfigurationProperties(“spring.datasource.username”)Spring Boot使用一种非常特殊的 PropertySource 命令, 该命令旨在允许对值进行合理的覆盖, 属性按以下顺序考虑:Devtools全局设置属性在你的主目录( ~/.spring-boot-devtools.properties 当devtools处于激活状态时。测试中的 @TestPropertySource 注解测试中的 @SpringBootTest#properties 注解属性命令行参数来自 SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联JSON)的属性ServletConfig 初始化参数ServletContext 初始化参数java:comp/env 中的JNDI属性Java系统属性(System.getProperties())操作系统环境变量一个只有random.*属性的RandomValuePropertySource在你的jar包之外的特殊配置文件的应用程序属性(application-{profile}.properties 和YAML 变体)在jar中打包的特殊配置文件的应用程序属性(application-{profile}.properties 和YAML 变体)在你的jar包之外的应用程序属性(application.properties 和YAML 变体)打包在jar中的应用程序属性(application.properties 和YAML 变体)@PropertySource 注解在你的 @Configuration 类上默认属性(通过设置 SpringApplication.setDefaultProperties 指定)访问命令行属性在默认情况下, SpringApplication 会转换任何命令行选项参数 (也就是说,参数从 – 开始, 像 –server.port=9000) 到一个 property, 并将它们添加到Spring Environment 中, 如前所述, 命令行属性总是优先于其他属性源.如果不希望将命令行属性添加到 Environment 中, 你可以使用 SpringApplication.setAddCommandLineProperties(false) 禁用它们.应用程序属性文件SpringApplication 在以下位置从 application.properties 文件加载属性并将它们添加到Spring Environment :当前目录子目录的 /config当前目录类路径下 /config 包类路径的根目录列表按优先顺序排序(在列表中较高的位置定义的属性覆盖在较低位置定义的属性).特殊配置文件的属性我们可能在不同环境下使用不同的配置, 这些配置有可能是在同一个文件中或不同文件中.1.在相同文件中##################################### Determime which configuration be usedspring: profiles: active: “dev” # Mysql connection configuration(share) datasource: platform: “mysql” driverClassName: “com.mysql.cj.jdbc.Driver” max-active: 50 max-idle: 6 min-idle: 2 initial-size: 6 —##################################### for dev environmentspring: profiles: “dev” datasource: # mysql connection user(dev) username: “root” # mysql connection password(dev) password: “r9DjsniiG;>7”—##################################### for product environmentspring: profiles: “product” datasource: # mysql connection user(product) username: “root” # mysql connection password(product) password: “root”—##################################### for test environmentspring: profiles: “test” datasource: # mysql connection user(test) username: “root” # mysql connection password(test) password: “root"这样在配置完相同属性的时, 还可以对不同的环境进行不同的配置.2.多个配置文件.我们可以把特定环境的配置, 放入多个配置文件中, 但是要按照 application-{profile}.properties 格式. 如下图.spring.profiles.active 属性进行设置.我们也可以把配置文件放在 jar 外面, 使用 spring.config.location 属性进行设置.java -jar beetltest-0.0.1-SNAPSHOT.jar -spring.config.location=classpath:/application.properties ...

February 13, 2019 · 2 min · jiezi

使用 CODING 进行 Spring Boot 项目的集成

本文作者:CODING 用户 - 高文持续集成 (Continuous integration) 是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。成员之间的代码相互影响,可能会出现各种编译、运行的错误,为了避免提交代码影响到其他开发者,每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现错误,使得开发过程更加简单方便。通用的持续集成流程大体是像上图所示一样,借助 Jenkins 连通 Git 与 Docker 镜像仓库,为后续的持续部署做准备。而在「CODING 持续集成」中,可以省去其中很多环境部署的麻烦事,下面说一下我在 CODING 平台做的持续集成工作。我与 CODING 之缘CODING 是国内首个一站式云端软件服务平台,致力于通过技术创新推动软件开发升级转型,让开发更简单。将代码托管、项目管理、Cloud Studio、一键部署等开发工具集成到浏览器中,免除繁杂的开发环境部署,降低软件开发部署成本。最初了解 「CODING」 ,是我在开发微信小程序时,腾讯云推荐托管代码到 CODING 上,于是注册了一个 CODING 账号。开始使用时,主要以Web版开发工具为主,觉得只是一个 Eclipse 的 Che 版本,当时也自己部署一个 Eclipse 的 Che 版本。但部署 Che 版本时,才发现, CODING 其实比 Che 好用得不是一点半点。流畅性、易用性已经高出 Che 一个等级了,功能上也比 Che 更丰富。后来逐渐用起来了,发现 「CODING」 不只是 WebIDE ,还是 Git 、 Jenkins 、 Wiki 、敏捷开发工具、项目管理工具……现在持续集成功能出来了,可以免费试用15天,于是注册一个玩一玩。wencst 的个人主页「CODING 持续集成」基础操作首先需要创建企业账号;然后创建自己的项目;进入项目维护项目代码。本文所使用的源代码为本人开源的自动开发框架。Git 操作下面为 Git 的操作了,相信看文章的大部分人可以略过这一步。详细的 Git 步骤可以参考:《 CODING 中的 Git 操作》Git 操作主要为后续持续集成操作的触发器。持续集成持续集成操作的设置相对比较简单,按照提示一步步下来即可。有一块需要注意的,就是构建所用的分支,在配置持续集成时,需要选择构建触发方式,触发时间(代码上传时触发/手动触发),以及完成时邮件发送提醒(提醒触发者/不做任何事/只有失败时提醒)。对于多分支代码工程一定要注意,选择自己所需的配置。我这里所用的为系统默认配置,即当有人提交代码至 master 时触发构建,完成时总是发邮件提示开发者。「CODING 持续集成」提供了三套不同的 Jenkinsfile 模板供开发者使用:简易模板、并行模板、自定义模板。我这里选用简易模板,并稍作修改。pipeline { agent { label “default” } stages { stage(“检出”) { steps { sh ‘ci-init’ checkout( [$class: ‘GitSCM’, branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[url: env.GIT_REPO_URL]]] ) } } stage(“构建”) { steps { echo “构建中…” sh ‘mvn clean install’ echo “构建完成.” archiveArtifacts artifacts: ‘/target/*.jar’, fingerprint: true // 收集构建产物 } } stage(“Docker”) { steps { echo “Docker镜像生成中…” sh ‘cd wencst-generatorJPA/target && cp classes/Dockerfile . && docker build -t wencst/wencst-generatorJPA .’ echo “镜像生成完成.” sh ‘docker push wencst/wencst-generatorJPA’ echo “镜像上传完毕” } } } }更新 Jenkinsfile 后,代码 push 到对应的分支上,会自动执行构建,发现构建失败。点开后,查看构建失败的具体原因,输出与 maven 编译时输出的没有什么差别。原因提示: there is no POM in this directory。原来我中间还有一层目录,需要进入目录后才能编译。pipeline { agent { label “default” } stages { stage(“检出”) { steps { sh ‘ci-init’ checkout( [$class: ‘GitSCM’, branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[url: env.GIT_REPO_URL]]] ) } } stage(“构建”) { steps { echo “构建中…” sh ‘cd wencst-generatorJPA && mvn clean install’ echo “构建完成.” archiveArtifacts artifacts: ‘/target/.jar’, fingerprint: true // 收集构建产物 } } stage(“Docker”) { steps { echo “Docker镜像生成中…” sh ‘cd wencst-generatorJPA/target && cp classes/Dockerfile . && docker build -t wencst/wencst-generatorJPA .’ echo “镜像生成完成.” sh ‘docker push wencst/wencst-generatorJPA’ echo “镜像上传完毕” } } } }提交代码后,会自动执行构建。可以显示程序编译过程,可以显示每一步详细输出,可以增加状态徽标到相应的文档或网页中。可以说「CODING 持续集成」 想的是比较周到的,基本集成了绝大部分开源系统中相应的职能。构建使用默认的 https://repo.maven.apache.org 源,构建速度也还可以。至此持续集成完成,界面清晰整洁,并且可以对测试报告和构建结果进行下载,构建过程也会发邮件给相关人员。确实让开发更简单了。以前在做这一系列工作时,架构师起码要做几件事情:1.搭建 git 仓库2.搭建 jenkins3.在 git 仓库中增加 CI 配置4.邮箱配置「CODING 持续集成」为开发者省去了很多工作,除了构建过程中必要的工作以外,其他的基本一键搞定,不用关心各个组件的安装配置,环境情况,网络情况,存储备份等内容。Jenkinsfile 拆解重点解释一下 stages 部分,整体分为三个 stages:第一步为代码检出 stage(“检出”) { steps { sh ‘ci-init’ checkout( [$class: ‘GitSCM’, branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[url: env.GIT_REPO_URL]]] ) } }这一步检出项目中的代码到 jenkins 的 workspace 目录下,这一步是 「CODING 持续集成」 默认的配置,无需过多解释。第二步为编译构建 stage(“构建”) { steps { echo “构建中…” sh ‘cd wencst-generatorJPA && mvn clean install’ echo “构建完成.” archiveArtifacts artifacts: ‘**/target/.jar’, fingerprint: true } }这一步是执行代码编译,我所用的是 maven 编译 spring boot 工程, 「CODING 持续集成」 集成了 mvn 命令,可以直接执行 maven 操作。注意: jenkins 执行 sh 命令的根路径都是在当前 workspace 下,所以切换路径与 maven 编译命令要在同一个 sh 命令下。编译执行后,收集编译的产物,archiveArtifacts artifacts: ‘**/target/*.jar’, fingerprint: true这一步的意思是,将所有工程的 target 路径下的 jar 包都算作工程产物。第三步为 docker 镜像生成 stage(“Docker”) { steps { echo “Docker镜像生成中…” sh ‘cd wencst-generatorJPA/target && cp classes/Dockerfile . && docker build -t wencst/wencst-generatorJPA .’ echo “镜像生成完成.” sh ‘docker push wencst/wencst-generatorJPA’ echo “镜像上传完毕” } } 对于熟悉 docker 的人并不是很陌生,依旧使用 shell 命令来执行 docker build 操作。cd wencst-generatorJPA/target 首先切换路径到 jar 包所在目录。cp classes/Dockerfile . 拷贝 Dockerfile 到当前路径下。docker build -t wencst/wencst-generatorJPA . 执行 docker build 操作,用以创建 docker 镜像。docker push wencst/wencst-generatorJPA 将创建出来的 docker 镜像上传到 dockerhub 中去。总结整体来说 「CODING 持续集成」 想的很周全了,无论从易用性、美观度以及人性化角度上来说,做得都非常不错。下面着重说说我使用 「CODING 企业版」 的持续集成后的感受:满足了从开发到代码管理,到代码集成,到单元测试,甚至到后续部署,一站式管理;配置相对简单,只需配置 Jenkinsfile 即可完成,无需花费大量的人力物力来做各系统间的整合操作;系统集成后,会给开发人员发送邮件,报告集成成功或失败,这一点还是比较人性化的;「CODING 持续集成」平台集成了很多种命令,起码我用到的 mvn/java/docker/git 这一类的命令基本都集成在服务中了。希望 CODING 会越来越完善,越来越好! ...

February 13, 2019 · 2 min · jiezi

SpringBoot事物管理

本篇概述在上一篇中,我们基本已经将SpringBoot对数据库的操作,都介绍完了。在这一篇中,我们将介绍一下SpringBoot对事物的管理。我们知道在实际的开发中,保证数据的安全性是非常重要的,不能因为异常,或者服务中断等原因,导致脏数据的产生。所以掌握SpringBoot项目的事物管理,尤为的重要。在SpringBoot中对事物的管理非常的方便。我们只需要添加一个注解就可以了,下面我们来详细介绍一下有关SpringBoot事物的功能。创建Service 因为在上一篇中我们已经用测试用例的方式介绍了SpringBoot中的增删改查功能。所以在这一篇的事物管理,我们还将已测试用例为主。唯一不同之处,就是我们需要创建一个Service服务,然后将相关的业务逻辑封装到Service中,来表示该操作是同一个操作。下面我们简单的在Service中只添加一个方法,并且在方法中新增两条数据,并验证该Service是否成功将数据添加到数据库中。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /** * 保存用户信息 / public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); }} 测试用例:package com.jilinwula.springboot.helloworld;import com.jilinwula.springboot.helloworld.service.UserInfoService;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;@RunWith(SpringRunner.class)@SpringBootTestpublic class JilinwulaSpringbootHelloworldApplicationTests { @Autowired private UserInfoService userInfoService; @Test public void save() { userInfoService.save(); } @Test public void contextLoads() { }} 下面我们看一下数据库中的数据是否插入成功。 抛出数据库异常 我们看数据成功插入了。现在我们修改一下代码,让插入数据时,第二条数据的数据类型超出范围来模拟程序运行时发生的异常。然后我们看,这样是否影响第一条数据是否能正确的插入数据库。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东京东京东京东京东京东京东京东京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); }} 为了方便我们测试,我们已经将数据库中的username字段的长度设置为了10。这样当username内容超过10时,第二条就会抛出异常。下面为执行日志:aused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column ‘username’ at row 1 at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495) at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2058) at com.mysql.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:5158) at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2043) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:114) at com.sun.proxy.$Proxy90.executeUpdate(Unknown Source) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204) … 82 more 然后我们现在查一下数据库中的数据,看看第二条数据的异常是否会影响第一条数据的插入。 添加@Transactional事物注解 我们看第一条数据成功的插入,但这明显是错误的,因为正常逻辑是不应该插入成功的。这样会导致脏数据产生,也没办法保证数据的一致性。下面我们看一下在SpringBoot中怎么通过添加事务的方式,解决上面的问题。上面提到过在SpringBoot中使Service支持事物很简单,只要添加一个注解即可,下面我们添加完注解,然后在尝试上面的方式,看看第一条数据还能否添加成功。然后我们在详细介绍该注解的使用。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东京东京东京东京东京东京东京东京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); }} 代码和之前基本一样,只是在方法上新增了一个@Transactional注解,下面我们继续执行测试用例。执行日志:Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column ‘username’ at row 1 at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495) at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2058) at com.mysql.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:5158) at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2043) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:114) at com.sun.proxy.$Proxy90.executeUpdate(Unknown Source) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204) … 92 more 日志还是和之前一样抛出异常,现在我们在查一下数据库中的数据。 发现数据库中已经没有第一条数据的内容了,这就说明了我们的事物添加成功了,在SpringBoot项目中添加事物就是这么简单。手动抛出异常 下面我们来测试一下,手动抛出异常,看看如果不添加@Transactional注解,数据是否能成功插入到数据库中。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); }} 我们在代码最后写了一个除以0操作,所以执行时一定会发生异常,然后我们看数据能否添加成功。执行日志:java.lang.ArithmeticException: / by zero at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:32) at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:19) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) 继续查看数据库中的数据。 我们发现这两条数据都插入成功了。我们同样,在方法中添加@Transactional注解,然后继续执行上面的代码在执行一下。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); }} 执行日志:java.lang.ArithmeticException: / by zero at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:33) at com.jilinwula.springboot.helloworld.service.UserInfoService$$FastClassBySpringCGLIB$$230fe90e.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671) at com.jilinwula.springboot.helloworld.service.UserInfoService$$EnhancerBySpringCGLIB$$33f70012.save(<generated>) at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:19) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) 数据库中数据: 我们看数据又没有插入成功,这样就保证了我们事物的一致性。添加try catch 下面我们将上述的代码添加try catch,然后在执行上面的测试用例,查一下结果。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() { try { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); } catch (Exception e) { log.info(“保存用户信息异常”, e); } }} 执行日志:2019-01-25 11:21:45.421 INFO 8654 — [ main] c.j.s.h.service.UserInfoService : 保存用户信息异常java.lang.ArithmeticException: / by zero at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:36) ~[classes/:na] at com.jilinwula.springboot.helloworld.service.UserInfoService$$FastClassBySpringCGLIB$$230fe90e.invoke(<generated>) [classes/:na] at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) [spring-core-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) [spring-tx-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) [spring-tx-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) [spring-tx-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at com.jilinwula.springboot.helloworld.service.UserInfoService$$EnhancerBySpringCGLIB$$5284ede6.save(<generated>) [classes/:na] at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:19) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_191] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_191] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_191] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_191] at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) [junit-4.12.jar:4.12] at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12] at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) [junit-4.12.jar:4.12] at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runner.JUnitCore.run(JUnitCore.java:137) [junit-4.12.jar:4.12] at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) [junit-rt.jar:na] at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) [junit-rt.jar:na] at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) [junit-rt.jar:na] at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) [junit-rt.jar:na] 查看数据库中的数据: 我们发现数据成功的插入了,虽然我们添加了@Transactional事物注解,但数据还是添加成功了。这是因为@Transactional注解的处理方式是,检测Service是否发生异常,如果发生异常,则将之前对数据库的操作回滚。上述代码中,我们对异常try catch了,也就是@Transactional注解检测不到异常了,所以该事物也就不会回滚了,所以在Service中添加try catch时要注意,以免事物失效。下面我们手动抛出异常,来验证上面的说法是否正确,也就是看看数据还能否回滚。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() throws Exception { try { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); } catch (Exception e) { log.info(“保存用户信息异常”, e); } throw new Exception(); }} 执行日志:java.lang.Exception at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:40) at com.jilinwula.springboot.helloworld.service.UserInfoService$$FastClassBySpringCGLIB$$230fe90e.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671) at com.jilinwula.springboot.helloworld.service.UserInfoService$$EnhancerBySpringCGLIB$$43d47421.save(<generated>) at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:20) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) 查看数据库结果: @Transactional注解的底层实现 我们发现,数据库中居然成功的插入值了,这是为什么呢?上面不是说,在抛出异常时,@Transactional注解是自动检测,是否抛出异常吗?如果抛出了异常就回滚之前对数据库的操作,那为什么我们抛出了异常,而数据没有回滚呢?这是因为@Transactional注解的确会检测是否抛出异常,但并不是检测所有的异常类型,而是指定的异常类型。这里说的指定的异常类型是指RuntimeException类及其它的子类。因为RuntimeException类继承了Exception类,导致Exception类成为了RuntimeException类的父类,所以@Transactional注解并不会检测抛出的异常,所以,上述代码中虽然抛出了异常,但是数据并没有回滚。下面我们继续修改一下Service中的代码,将代码中的异常类修改为RuntimeException,然后在看一下运行结果。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() throws RuntimeException { try { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); } catch (Exception e) { log.info(“保存用户信息异常”, e); } throw new RuntimeException(); }} 我们就不看执行的日志了,而是直接查数据库中的结果。 我们看数据没有插入到数据库中,这就说明了,事物添加成功了,数据已经成功的回滚了。在实际的开发中,我们常常需要自定义异常类,来满足我们开发的需求。这时要特别注意,自定义的异常类,一定要继承RuntimeException类,而不能继承Exception类。因为刚刚我们已经验证了,只有继承RuntimeException类,当发生异常时,事物才会回滚。继承Exception类,是不会回滚的。这一点要特别注意。@Transactional注解参数说明 下面我们介绍一下@Transactional注解的参数。因为刚刚我们只是添加了一个@Transactional注解,实际上在@Transactional注解中还包括很多个参数,下面我们详细介绍一下这些参数的作用。 @Transactional注解参数说明:参数作用value指定使用的事务管理器propagation可选的事务传播行为设置isolation可选的事务隔离级别设置readOnly读写或只读事务,默认读写timeout事务超时时间设置rollbackFor导致事务回滚的异常类数组rollbackForClassName导致事务回滚的异常类名字数组noRollbackFor不会导致事务回滚的异常类数组noRollbackForClassName不会导致事务回滚的异常类名字数组 下面我们只介绍一下部分参数,因为大部分参数实际上是和Spring中的注解一样的,有关Spring事物相关的内容,我们将在后续的文章中在做介绍,我们暂时介绍一下rollbackFor参数和noRollbackFor参数。(备注:rollbackForClassName和noRollbackForClassName与rollbackFor和noRollbackFor作用一致,唯一的区别就是前者指定的是异常的类名,后者指定的是类的Class名)。rollbackFor: 指定事物回滚的异常类。因为在上面的测试中我们知道@Transactional事物类只会回滚RuntimeException类及其子类的异常,那么实际的开发中,如果我们就想让抛出Exception异常的类回滚,那应该怎么办呢?这时很简单,只要在@Transactional注解中指定rollbackFor参数即可。该参数指定的是异常类的Class名。下面我们还是修改一下Servcie代码,抛出Exception异常,但我们指定rollbackFor为Exception.class,然后在看一下数据是否能回滚成功。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional(rollbackFor = Exception.class) public void save() throws Exception { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); throw new Exception(); }} 按照之前我们的测试结果我们知道,@Transactional注解是不会回滚Exception异常类的,那么现在我们指定了rollbackFor参数,那么结果如何呢?我们看一下数据库中的结果。 我们看数据库中没有任何数据,也就证明了事物添加成功了,数据已经的回滚了。这也就是@Transactional注解中rollbackFor参数的作用,可以指定想要回滚的异常。rollbackForClassName参数和rollbackFor的作用一样,只不过该参数指定的是类的名字,而不是class名。在实际的开发中推荐使用rollbackFor参数,而不是rollbackForClassName参数。因为rollbackFor的参数是类型是Class类型,如果写错了,可以在编译期发现。而rollbackForClassName参数类型是字符串类型,如果写错了,在编译期间是发现不了的。所以推荐使用rollbackFor参数。noRollbackFor: 指定不回滚的异常类。看名字我们就知道该参数是和rollbackFor参数对应的。所以我们就不做过多介绍了,我们直接验证该参数的作用。我们知道@Transactional注解会回滚RuntimeException类及其子类的异常。如果我们将noRollbackFor参数指定RuntimeException类。那么此时事物应该就不会回滚了。下面我们验证一下。下面为Service代码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 */ @Transactional(noRollbackFor = RuntimeException.class) public void save() throws Exception { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); throw new RuntimeException(); }} 我们查看一下数据库中是否成功的插入了数据。 我们看数据库中成功的插入数据了,也就证明了@Transactional注解的noRollbackFor参数成功了,因为正常来说,数据是会回滚的,因为我们抛出的是RuntimeException异常。数据没有回滚也就说明了,参数成功。noRollbackForClassName参数和noRollbackFor参数一样,只是一个指定的是class类型,一个指定的是字符串类型。所以,为了在编译期间发现问题,还是推荐使用noRollbackFor参数。 上述内容就是SpringBoot中的事物管理,如有不正确的欢迎留言,谢谢。项目源码 https://github.com/jilinwula/…原文链接 http://jilinwula.com/article/… ...

January 31, 2019 · 5 min · jiezi

Spring Boot入门篇

前篇 很长时间不写博客了,究其原因则是这几个月工作及生活都发生了很多事情,导致不得分心处理这些。最近难得忙里偷闲,决定还是继续更新吧。毕竟一件事情做久了,如果突然中断,心中难免有些遗憾。由于博客之前更新的内容均是Redis相关的,本打算继续把后续的Redis内容更新出来,但无奈因为这段时间的中断,发现Redis的思路已经断了,所以决定还是很把Redis放一放吧,沉淀一段时间之后,在将后续的内容补充上。虽然这段时间没有更新博客,但在技术角度来说,还是有所收获的,因为公司最近一直在使用Spring Boot,虽然Spring Boot很火,但自己一直没有真正在项目中使用过,正好有这样的机会,并且在使用的过程中遇到了各种各样的问题,我想那就把工作中遇到的种种问题,更新出来吧。所以,接下来,本人将不定期的更新Spring Boot相关的内容。由于Spring Boot实在是太火了,网上有很多相关的资料及书籍。所以,本博客的更新重点,将以实用为主,相关的理论方面的内容,请参考,官方文档,及相关书籍。创建SpringBoot好了,言归正传,我们来学习Spring Boot的第一篇文章,也就是入门篇。我们首先创建一个Spring Boot项目。具体操作如下图所示: 创建Spring Boot的项目和创建Spring的项目不同,在上图中我们不能选择Maven创建项目,而是使用IDEA中Spring Initializr创建Spring Boot项目。因为它会为我们直接生成Spring Boot的项目架构。在Spring Initializr选项中我们看到默认使用了https://start.spring.io>这个…这个域名地址,来生成我们的项目架构。下图就是我们直接访问上述域名来生成项目架构。 因为上图中的配置和IDEA中的Spring Initializr配置基本一样,所以上图中的创建方式,就不做详细介绍了,我们继续介绍Spring Initializr方式的配置。 上图中的选项比较多,下面我们详细介绍一下:Group:同Maven中的Group一样,也就是项目唯一标识Artifact:同Maven中的Artifact一样,通常为项目名Type:项目的Maven类型,我们默认选择就可以Language:项目的开发语言,那结果当然选择Java喽Packaging:打包类型jar或者war,因为SpringBoot可以支持这两种方式启动,所以,这两种选择哪个都可以Java Version:Java的版本号,推荐使用1.8版本Version:项目的版本号Name:项目名,推荐和Artifact一致Description:项目描述Package:项目包的名字 这一步我们选择SpringBoot的版本,及项目的依赖包,这里要注意因为SpringBoot2.0版本和1.0版本相差甚大,所以,暂时推荐使用1.0版本。除此之外,因为创建的是web项目,所以,我还要要添加和web相关的依赖,在这点和Maven创建Spring项目不同,我们只需要选择,一个web的依赖就可以了,SpringBoot会自动把这个web相关的依赖都下载好,这也就是SrpingBoot的优势之一,比较方便。当然如果我们开发一下完整的项目,还是需要很多其它的项目依赖的,这里我们不用着急,暂时只添加web这个就可以,如果需要其它的依赖,我们还是可以修改的。好的我们继续下面操作: 这一步我们只要选择完成则可以了。这样我们的SpingBoot项目就创建好了,下图就是项目架构图: 当项目第一次创建后,右下方,会有上图中的两个提示选项,我们只要选择第二个就可以,这样,当我们修改项目中pom.xml文件添加依赖时,IDEA会自动添加我们的依赖。 启动SpringBoot 上图就是SrpingBoot生成的项目结构图,默认会创建两个类,一个是启动类,一个是测试类。和Spring项目不同,我们不需要配置Tomcat来启动SrpingBoot项目,我们直接使用启动类,即可启动SrpingBoot项目。下面我们尝试启动一下,因为启动类就是一个main方法,所以我们只要直接执行就可以了。因为SrpingBoot项目的默认端口为8080,所以我们启动后可以直接访问8080端口,来验证SrpingBoot是否启动成功。 上图就是我们访问8080端口后的结果。虽然返回的结果报错,但这恰恰说明了我们的项目启动成功了,否则就会报404错误。那为什么会报上面的错误呢?这是因为我们没有写controller,下面我们写一个简单的controller来看一下上面的问题还有没有。下面为controller代码。package com.jilinwula.springboot.helloworld;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/jilinwula")public class JilinwulaController { @RequestMapping("/helloworld") public Object helloWorld() { return “吉林乌拉”; }}启动方式 下面我们访问http://localhost:8080/jilinwu…地址。下图为访问该地址的返回结果。 下面我们看一下SpringBoot的启动方式,上面说过,我们可以不用Tomcat直接启动SpringBoot项目,也就是直接启动main方法,当然我们一样可以使用Tomcat的方式启动SpringBoot项目,我们可以直接将SpringBoot项目项目打包成war放到Tomcat中就可以了。具体操作如下:在SpringBoot项目中的pom.xml中添加如下配置: <packaging>war</packaging> 然后执行以下打包命令: mvn clean install这样在我们的项目中就会生成一个target包里面就会项目的中war包,只要把这个war包放到Tomcat中即可。第二种方式就是直接在项目中使用java -jar 项目名.jar方式启动项目。 还有一种方式就是直接在项目中mvn spring-boot:run命令,也可以正常启动SpringBoot项目。 以上内容就是SpringBoot的入门篇,在下一篇中我们将分享,在SpringBoot中的个性化默认配置。项目源码https://github.com/jilinwula/jilinwula-springboot-helloworld原文链接http://jilinwula.com/article/24336

January 31, 2019 · 1 min · jiezi

SpringBoot个性化配置

在上一篇中我们简单的介绍了SpringBoot项目的创建及其启动方式。在这一篇中我们主要介绍一下SpringBoot项目的个性化配置。因为通过上一篇中知识我们知道SpringBoot项目的默认端口为8080,那如果我要修改这个默认端口,应该怎么改呢?又比如SpringBoot项目在启动时,默认是没有项目名字的,那如果我们想要添加自己喜欢的项目名字又该怎么办呢?这些都在这一篇的内容中。好了,下面我们详细介绍一下怎么修改SpringBoot项目中的默认配置。修改默认端口 在上一篇的SpringBoot项目中我们看到在resources目录中有一个application.properties文件,这个文件就是让我们个性化配置SpringBoot项目参数的,也就是说,在这个文件中按照SpringBoot为我们提供的参数名,就可以直接修改SpringBoot项目的默认参数。下面我们尝试修改SpringBoot项目的默认端口。具体修改如下: 在application.properties文件中添加下面的参数,然后,启动application.properties文件项目即可。server.port=8081 并且如果我们使用IDEA开发工具时,当我们在在application.properties文件中输入参数时,IDEA就会自动为我们提供相关参数提示,这样方便我们修改。也就是如下图所示: 这时我们启动SpringBoot项目并且用8080端口访问项目时,发现已经找不到服务了。 而如果我们用访问8081端口访问项目,则发现服务可以正常访问。这就说明,我们已经成功将SpringBoot项目的默认端口修改为8081端口了。 虽然上面的方式已经成功的修改了SpringBoot项目的默认参数,但在实际的开发中,并不推荐使用application.properties文件的方式修改,因为在SpringBoot项目中有更推荐的方式。也就是使用yml文件的方式。application.yml文件 使用yml文件的方式修改默认参数,也比较简单,也就是把application.properties文件文件修改为application.yml文件即可。唯一不同的方式,就是yml文件有自己独特的语法,和properties文件不同,可以省略很多参数,并且浏览比较直观。下面我们尝试用yml文件的方式,将SpringBoot的端口修改为8082端口。 启动项目后访问刚刚的8081端口,发现项目已经访问不了。 这时我们访问8082端口,发现项目访问又正常了,这就说明我们使用yml的方式修改SpringBoot的默认参数方式成功了。 如果我们访问http://localhost:8082/jilinwu…地址,即可看到SpringBoot接口返回的数据。 修改默认项目名 下面我们还将使用yml的方式配置SpringBoot项目的项目名。具体参数如下:server: port: 8082 context-path: /springboot 我们继续启动项目然后依然访问http://localhost:8082/jilinwu…地址,这时发现接口访问失败。 然后我们访问http://localhost:8082/springb…地址,发现服务又可正常访问了。 获取配置文件中参数 在实际的项目开发中,我们通常会遇到,读取配置文件中的参数,那么在SpringBoot中怎么获取配置文件中的参数呢?下面我们在配置文件中添加如下参数。server: port: 8082 context-path: /springbootemail: username: jilinwula password: 123456 下面我们在Controller中采用如下的方式读取配置文件中的参数。@RestController@RequestMapping("/jilinwula")public class JilinwulaController { @Value("${email.username}") private String username; @Value("${email.password}") private String password; @RequestMapping("/helloworld") public Object helloWorld() { Map<String, Object> map = new HashMap<String, Object>(); map.put(“username”, username); map.put(“password”, password); return map; }} 我们可以直接使用@Value注解来获取配置文件中的参数,并且这个注解不只是在SpringBoot中可以使用,这个注解在Spring的项目中也可以使用。下面我们启动项目,并访问http://localhost:8082/springb…地址,看看是不是可以成功的获取配置文件中的参数。 我们看上图所示,我们成功的获取到了配置文件中的参数。但如果有强迫证的人,对于上面的代码难免有些不满意。因为如果我们要获取配置文件中非常多的参数时,要是按照上面的代码编写,则需要在代码中编写大量的@Value注解,这显然是不合理的。那有没有比较方便的办法呢?答案一定是有的,并且SpringBoot为我们提供了非常方便的方法获取配置文件中的参数。下面我们看一下这种方式。 我们首先要在项目的pom.xml中添加以下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> 第一个依赖是自动获取配置文件参数的必须依赖,而下面的依赖,则是可以用注解的方式动态生成get和set方法,这样我们在开发时,就不用在写get和set方法了,在实际的项目中比较常用。在使用lombok生成get和set方法时,还要在IDEA中添加相应的lombok插件,否则IDEA会提示到不到get和set方法的警告。 其次我们新创建一下获取配置参数的类,并且添加@ConfigurationProperties注解,该注解会自动将配置文件中的参数注入到类中的属性中(不需要写@Value注解)。并且可以指定prefix参数来指定要获取配置文件中的前缀。具体代码如下:package com.jilinwula.springboot.helloworld;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;@Component@ConfigurationProperties(prefix = “email”)@Datapublic class EmailProperties { private String username; private String password;} 上面中的@Data,注解就是动态生成get和set方法的所以上述的代码是不需要写get和set方法的。下面我们看一下Controller中的代码修改:package com.jilinwula.springboot.helloworld;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/jilinwula")public class JilinwulaController { @Autowired private EmailProperties emailProperties; @RequestMapping("/helloworld") public Object helloWorld() { Map<String, Object> map = new HashMap<String, Object>(); map.put(“username”, emailProperties.getUsername()); map.put(“password”, emailProperties.getPassword()); return map; }} 下面我们启动项目并访问接口看看是否能够成功获取配置文件中的参数。 ) 下面我们介绍一下在SpringBoot中怎么处理不同环境中获取不同的配置参数。下面我们模拟两人环境一个是开发环境,一个是测试环境,我们暂时以不同端口来区分这两个环境的区别。 application-dev.yml:server: port: 8081 context-path: /springbootemail: username: jilinwula password: 123456 application-test.yml:server: port: 8082 context-path: /springbootemail: username: jilinwula password: 654321 application.yml:spring: profiles: active: dev 这样当我们在application.yml文件中的参数设置为dev时,SpringBoot项目在启动时就会读取application-dev.yml中的参数。如果我们将参数设置为test时,则SpringBoot会读取application-test.yml文件中的参数。 下面我们分别启动项目并且访问接口:当参数为dev: 当参数为test: 启动时指定参数 在上一篇中我们已经介绍过了我们可以使用java -jar 项目的名字的方式启动SpringBoot项目。并且,该方式还支持指定SpringBoot参数,例如上面刚刚介绍的指定获取同环境的配置参数。具体命里如下:java -jar jilinwula-springboot-helloworld-0.0.1-SNAPSHOT.jar –spring.profiles.active=dev 我们此时继续访问接口发现还是成功的获取了dev环境中的参数。 上述内容就是SpringBoot个性化配置的内容,如有不正确,或者需要交流的,欢迎留言,谢谢。项目源码:https://github.com/jilinwula/…原文链接:http://jilinwula.com/article/… ...

January 31, 2019 · 1 min · jiezi

SpringBoot数据库操作

本篇概述上一篇中我们已经介绍了在SpringBoot项目中怎么修改默认配置参数,并且我们还掌握了怎么获取配置文件中自定义参数。在这一篇中我们将介绍SpringBoot对数据库的操作。既然是对数据库的操作,那难免有一些配置的参数。例如数据库的连接、数据库账号及数据库密码等。所以掌握上篇中的内容很重要。除此之外,我们还要介绍一下用什么样的技术来操作数据库。操作数据库的技术有很多例如比较常见的JDBC、Mybatis、Hibernate等。在SpringBoot的项目中为我们提供了另外一种操作数据库的技术,也就是JPA。我们可以通过JAP中提供的方式来非常方便的操作数据库。下面我们首先添加SpringBoot对数据库的配置参数。具体参数如下:数据库配置spring: profiles: active: dev datasource: url: jdbc:mysql://localhost:3306/springboot?useSSL=false&characterEncoding=utf8 username: root password: jilinwula driver-class-name: com.mysql.jdbc.Driver jpa: hibernate: ddl-auto: create show-sql: truespring.jpa.hibernate.ddl-auto参数详解 上面的配置参数比较简单,我们就不详细介绍了,我们只介绍spring.jpa.hibernate.ddl-auto参数。该参数的作用是自动操作表结构。且该参数有4种选项,下面我们详细介绍一下这4种的区别。create: 当我们启动SpringBoot项目时,会自动为我们创建与实体类对应的表,不管表中是否有数据。也就是如果指定参数为create时,当项目启动后,该表的数据一定为空。因为该参数的处理方式,是先将表删除后,然后在创建新的表。create-drop: 当我们启动项目时也会我们自动创建表,但当我们的项目运行停止后,它会自动为我们删除表,并且该参数为create一样在启动时也会先把表删除后,然后在创建。update: 每当我们启动项目时,如果表不存在,则会根据实体类自动帮我们创建一张表。如果表存在,则会根据实体类的变化来决定是不是需要更新表,并且不管更不更新表都不会清空原数据。validate: 当我们启动项目时会验证实体类中的属性与数据库中的字段是否匹配,如不匹配则报错。添加相关依赖 如果我们按照上面的方式配置完后,则会发现上面的driver-class-name参数会报红显示,原因是没有找到相关的依赖。并且在SpringBoot的项目中如果想用JPA功能的除了要引入Mysql的依赖外,还要引入Jpa的依赖。具体依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope></dependency> 当我们添加完上面的依赖后发现配置文件中的driver-class-name参数已经不报红显示了,这就表示我们的依赖引入成功了。spring.jpa.hibernate.ddl-auto参数验证 下面我们创建一个实体类,来验证一下,上面的spring.jpa.hibernate.ddl-auto参数是不是上面说的那样。我们首选验证当参数为create时。下面为实体类的代码:package com.jilinwula.springboot.helloworld.entity;import lombok.Data;import javax.persistence.;@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password;} 上面实体类中我们指定了几个注解,下面我们详细介绍一下它们的作用:实体类详解@Data: 自动生成GET和SET方法的注解,在上一篇中我们已经介绍过了,该注解可以在类上和属性中添加。如果添加在类上,是表示自动为该实体类的所有属性创建GET和SET方法。如果添加到属性中则表示只会为该属性添加GET和SET方法。这样我们就没必要为代码中生成大量的GET和SET烦恼了。这时可能有人会想到那如果我只想让它帮我生成GET方法或者只想生成SET方法时应该怎么办呢?别着急,既然你想到了,那么开发这个注解的人也想到了,我们只需要将上面的@Data注解修改为相应的@Getter或者@Setter注解即可。它们正好对应的生成GET和SET方法。@Entity: 实体类注解。只有标识该注解的类,JPA才能自动将这个类中的属性和数据库进行映射。@Table: 标识该实体类和数据库中哪个表进行映射。@Id: 标识该自动为主键标识。@GeneratedValue: 标识主键的生成规则。这个在后面的文章中在做详细介绍。 现在我们一切准备就绪了,我们只要启动一下SpringBoot的项目就可以了,看看会不会自动为我们创建一张userinfo的表。(备注:数据库需要我们自己创建)。我们首先确认一下数据库中确实没有userinfo这张表。 下面我们启动一下SpringBoot的项目,看一下数据库中有没有创建新的表。 我们看JPA确实为我们创建了一张和实体类中@Table注解指定的表,并且表中的字段和实体类中的属性一致。下面我们手动向表中添加一条数据,然后重新启动项目,看看项目启动后,这条新增的数据还是否存在。 我们只新增了一条数据,然后重启启动项目后,在看一下数据中的userinfo表,看看该数据还有没有。 我们发现刚刚添加的那条数据已经没有了,这也就恰恰证明了,我们上面所有说当spring.jpa.hibernate.ddl-auto参数为create时,每当项目启动时,都会将原先的表删除,然后在通过实体类重新生成新的表,既然已经是将原先的表都删除了,那曾经添加的数据当然不存在了。如果我们仔细查看SpringBoot项目的启动日志,发现启动日志中已经输出了相应的删除及建表的语句,下面为项目启动的时操作表的日志。Hibernate: drop table if exists user_infoHibernate: create table user_info (id bigint not null auto_increment, password varchar(255), username varchar(255), primary key (id)) 下面我们将spring.jpa.hibernate.ddl-auto参数修改为create-drop来验证一下create-drop参数的特性。我们首先先把表删除掉,以免刚刚的create参数影响create-drop的验证。 我们还是和刚刚一样启动项目后查看数据库中是不是自动为我们创建一张userinfo表。 我们看一样还是自动为我们创建了一个新的表。下面我们将服务执行后,在看一下该表还是不是存在。 我们发现,刚刚创建的表已经自动删除了,这就是create-drop参数的特性。我们查看日志,也可以发现当停止服务时,自动执行的删表语句。下面的日志。Hibernate: drop table if exists user_infoHibernate: create table user_info (id bigint not null auto_increment, password varchar(255), username varchar(255), primary key (id))2019-01-18 16:56:27.033 INFO 6956 — [ main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete2019-01-18 16:56:27.058 INFO 6956 — [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit ‘default'2019-01-18 16:56:27.400 INFO 6956 — [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@21de60b4: startup date [Fri Jan 18 16:56:23 CST 2019]; root of context hierarchy2019-01-18 16:56:27.482 INFO 6956 — [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped “{[/error]}” onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)2019-01-18 16:56:27.483 INFO 6956 — [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped “{[/error],produces=[text/html]}” onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)2019-01-18 16:56:27.511 INFO 6956 — [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2019-01-18 16:56:27.512 INFO 6956 — [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2019-01-18 16:56:27.553 INFO 6956 — [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2019-01-18 16:56:27.888 INFO 6956 — [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup2019-01-18 16:56:27.973 INFO 6956 — [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)2019-01-18 16:56:27.978 INFO 6956 — [ main] JilinwulaSpringbootHelloworldApplication : Started JilinwulaSpringbootHelloworldApplication in 5.928 seconds (JVM running for 7.512)2019-01-18 17:00:50.630 INFO 6956 — [ Thread-16] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@21de60b4: startup date [Fri Jan 18 16:56:23 CST 2019]; root of context hierarchy2019-01-18 17:00:50.661 INFO 6956 — [ Thread-16] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown2019-01-18 17:00:50.664 INFO 6956 — [ Thread-16] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit ‘default'2019-01-18 17:00:50.666 INFO 6956 — [ Thread-16] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000227: Running hbm2ddl schema exportHibernate: drop table if exists user_info 在日志中我们发现一共执行了两次删除表的语句,第一次是在启动前,第二次是在服务停止时。 下面我们把spring.jpa.hibernate.ddl-auto参数修改为update来验证update参数的特性。同样我们还是事先要把刚刚生成的表删除掉,因为create-drop参数在停止服务时,已经把刚刚的表删除掉了,所以我们就不用手动删除了,我们直接把spring.jpa.hibernate.ddl-auto参数修改为update,然后直接启动项目。 我们看上图当我们把spring.jpa.hibernate.ddl-auto参数设置为update时,也会自动为我们创建表。并且我们停止服务时,该表依然还存在,并且不会清除数据。下面我们向表中添加一条新数据。然后修改实体类中的结构,看看刚刚新增的数据还存在不存在。 实体类修改如下:package com.jilinwula.springboot.helloworld.entity;import lombok.Data;import javax.persistence.;@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String nickname;} 我们新增了一个nickname属性,然后我们重新启动服务看看这个新的字段会不会自动在表中创建,并且没有更改原先表中的数据。 我们看新的字段已经自动创建成功了,并且没有删除原先表中的数据,这就是update参数的作用,在实际的开发中,把spring.jpa.hibernate.ddl-auto参数设置为update,是比较常见的配置方式。 下面我们验证最后一个参数也就是validate参数。因为之前的操作我们现在userinfo表中一其有3个字段,现在我们将实体类中的nickname字段注释掉,然后我们在启动服务,看一看项目启动是否正常。下面为实体类源码:import javax.persistence.;@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password;// private String nickname;} 当我们启动项目时,发现项目是可以正常启动。然后查看数据库中表,我们发现表中的结构没有任何变化。也就是说,我们注释掉数据库中已有的字段然后启动项目时,项目是可以正常启动的。这又是为什么呢?validate参数的作用不就是验证实体类中的属性与数据库中的字段不匹配时抛出异常吗?为什么当我们这么设置时没有抛出异常呢?这是因为validate参数的特性是只有实体类中的属性比数据库中的字段多时才会报错,如实体类中的属性比数据库中字段少,则不会报错。刚刚我们将nickname属性给注释了,但validate是不会更改表结构的,所以数据库中还是会有nickname字段的,这就导致数据中的字段比实体类中的属性多,所以当我们启动项目时是不会抛出异常。但反之,如果我们在实体类中新增一个字段,然后我们在启动项目时,项目就会抛出异常。实体类源码:@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String nickname; private Long roleId;} 我们新增了一个roleId字段,并且该字段在数据库中是没有的,然后我们启动项目。查看日志发现项目已经启动失败了。下面为日志:Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [role_id] in table [user_info] at org.hibernate.tool.schema.internal.SchemaValidatorImpl.validateTable(SchemaValidatorImpl.java:85) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.tool.schema.internal.SchemaValidatorImpl.doValidation(SchemaValidatorImpl.java:50) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.tool.hbm2ddl.SchemaValidator.validate(SchemaValidator.java:91) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:475) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:444) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:879) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final] at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:360) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:384) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE] … 20 common frames omitted 下面我们在数据库中将roleId字段手动添加上,然后我们在重新启动项目,在看一下启动时项目还是否报错。 下面我们重新启动项目,然后在看一下日志,发现项目已经成功启动了,这就是validate参数的作用。2019-01-19 17:17:41.160 INFO 1034 — [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup2019-01-19 17:17:41.201 INFO 1034 — [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)2019-01-19 17:17:41.204 INFO 1034 — [ main] JilinwulaSpringbootHelloworldApplication : Started JilinwulaSpringbootHelloworldApplication in 2.596 seconds (JVM running for 3.233)实体类注解高级配置 上述内容我们基本已经将spring.jpa.hibernate.ddl-auto参数的的使用介绍完了,下面我们介绍一下实体类中的高级注解。因为我们在上面的测试中我们发现,当我们把spring.jpa.hibernate.ddl-auto参数设置为create时,虽然成功的创建了实体类中指定的表,但是我们发现自动创建的表只是字段和实体类中的属性一致,但例如表中的字段长度、字段的描述、表的索引,这些高级的配置,是需要我们在实体类中添加新的注解,才能设置的。下面我们将实体类中的代码修改一下,添加上面说的注解,并且验证上面注解是否可以正确设置表中的长度、描述及索引。(备注:别忘记将spring.jpa.hibernate.ddl-auto参数设置为create)下面为实体类源码:package com.jilinwula.springboot.helloworld.entity;import lombok.Data;import javax.persistence.;@Data@Entity@Table(name = “user_info”, indexes = @Index(columnList = “username”))public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = “id”, columnDefinition = “bigint(10) comment ‘主键’”) private Long id; @Column(name = “username”, columnDefinition = “varchar(10) not null default ’’ comment ‘账号’”) private String username; @Column(name = “password”, columnDefinition = “varchar(10) not null default ’’ comment ‘密码’”) private String password; @Column(name = “nickname”, columnDefinition = “varchar(10) not null default ’’ comment ‘妮称’”) private String nickname; @Column(name = “role_id”, columnDefinition = “bigint(10) not null default 0 comment ‘角色’”) private Long roleId;} 上面我们介绍过当在类中添加@Entity注解后,JPA会自动将实体类中的属性映射为数据库中表里的字段。但在实际的开发中我们可能会遇到实体类中的属性与数据库中的字段不一致的情况。这时我们就要使用@Column注解了,该注解的参数有很多,我们要掌握2个就可以了。一个参数为name因为JAP在映射属性到数据库时,如果没有指定@Column参数,则默认使用和实体类中的属性一样的名字,如果指定了@Column则使用该注解中的name参数。第二个参数为columnDefinition参数,该参数则是可以直接将我们创建表中的语句写在该参数中,这样我们可以很方便的控制字段的长度及类型。还有一个注解为@indexes。该注解可指定我们指定属性为表中的索引,这里要注意一下如果表中字段名字和实体类中的属性名字不一致,@indexes注解需要指定的是实体类中的属性名,则不是真正表中的字段名。下面我们启动项目,看一下数据库中的表结构是不是和我们实体类中映射的一样。。 我们现在看数据库中的映射,除了创建索引时自动生成的索引名不一样,其它的字段映射类型长度及其描述都和我们实体类中的一致。JpaRepository接口 下面我们介绍一下怎么使用JPA来操作数据库。我们以增删改查为例,分别介绍它们的使用。在使用JAP操作数据库时,我们需要创建一个和实体类相对应的接口,并且让该接口继承JAP中已经提供的JpaRepository(有很多个接口暂时只介绍这一个)接口。这样我们就可以通过这个接口来操作数据库了。下面我们看一下该接口的源码。package com.jilinwula.springboot.helloworld.Repository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;@Repositorypublic interface UserInfoRepository extends JpaRepository<UserInfoEntity, Long> {} 在我们继承JpaRepository接口时需要我们指定两个参数,第一个参数表示我们要操作的实体类是哪一个,第二个参数表示我们实体类中的主键类型,其次我们还需要添加@Repository注解,这样JPA才能操作数据库。下面我们创建一个测试用例,分别介绍数据库中的增删改查。下面为测试用例源码:package com.jilinwula.springboot.helloworld;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;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;@RunWith(SpringRunner.class)@SpringBootTestpublic class JilinwulaSpringbootHelloworldApplicationTests { @Autowired private UserInfoRepository userInfoRepository; @Test public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“吉林乌拉”); userInfoEntity.setPassword(“jilinwula”); userInfoEntity.setNickname(“二十四分之七倍根号六”); userInfoEntity.setRoleId(1L); userInfoRepository.save(userInfoEntity); } @Test public void contextLoads() { }}数据库的增删改查 我们暂时只写一个新增的方法,并且我们发现,虽然我们的UserInfoRepository接口中没有写任何方法,但我们居然可以直接调用save方法了。这是因为当我们将UserInfoRepository接口继承JpaRepository接口时,是默认继承了该接口的一些方法,所以这些基本的增删改查操作,是不需要我们写任何代码的。下面我们执行一下测试用例,看看该条数据能否正确的插入到数据库中。 我们看数据已经成功的添加到了数据库中。下面我们在添加一条,方便我们以后测试。 。下面我们编写一下查询语句,看看能否正确查出数据。@Test public void select() { UserInfoEntity userInfoEntity = userInfoRepository.findOne(1L); System.out.println(userInfoEntity); } 我们看同样,我们还是没有写findOne方法,但是我们居然可以直接使用。findOne方法是JPA为我们提供通过主键查询数据的方法,所以该方法的返回值是实体类对象,因为只能返回一条数据。下面我们执行一下该测试用例,看看能否正确查询出数据。UserInfoEntity(id=1, username=吉林乌拉, password=jilinwula, nickname=二十四分之七倍根号六, roleId=1) 我们看已经成功的查询出数据了。这时有人会说,如果我们想查所有的数据应该怎么办呢?别着急,JPA中除了提供了findOne方法,还提供了findAll方法,顾名思义,该方法就是查询所有数据的。既然是所有数据,所以该方法的返回值为List。下面为测试用例源码,及其执行日志。@Testpublic void selectAll() { List<UserInfoEntity> userInfoEntitys = userInfoRepository.findAll(); System.out.println(userInfoEntitys);}[UserInfoEntity(id=1, username=吉林乌拉, password=jilinwula, nickname=二十四分之七倍根号六, roleId=1), UserInfoEntity(id=2, username=阿里巴巴, password=alibaba, nickname=淘宝, roleId=2)] 下面我们介绍一下更新方法,在JPA中更新方法和save方法一样,唯一的区别就是如果我们在实体类中设置了主键,则调用sava方法时,JPA执行的就是更新。如果不设置主键,则JPA执行的就是新增。下面为测试用例源码:@Testpublic void update() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setId(1L); userInfoEntity.setUsername(“阿里巴巴”); userInfoEntity.setPassword(“alibaba”); userInfoEntity.setNickname(“淘宝”); userInfoEntity.setRoleId(2L); userInfoRepository.save(userInfoEntity);} 现在我们在查询一下数据库,如果更新语句成功,那么此时数据库中则会有两条一样的数据。 我们看,数据库中的确有两条一模一样的数据了,这就证明了我们刚刚的更新语句成功了。下面我们介绍一下最后一个删除语句,该语句也同样比较简单,因为JPA也同样为我们提供了该方法,下面为测试用例。 @Test public void delete() { userInfoRepository.delete(1L); } 我们在查询一下数据库,看看id为1的数据是否还在数据库中存在。 我们看该数据成功的删除了。这就是JPA对数据库的增删改查的基本操作。当然JPA中还提供了很多复杂的语法,例如级联查询、分页查询等等。这些高级的功能我们在后续的文章中在做详细介绍。这就是本篇的全部内容,如有疑问,欢迎留言,谢谢。项目源码 下面为项目源码:https://github.com/jilinwula/jilinwula-springboot-helloworld3原文链接 下面为项目源码:http://jilinwula.com/article/24338 ...

January 31, 2019 · 4 min · jiezi

Spring Boot系列实战文章合集(附源码)

概 述文章开始之前先感叹一番吧。个人从之前的 C语言项目开发转到 Java项目开发来之后开始学着用 Spring Boot做一些后端服务,不得不说 Spring Boot脚手架式的开发真的是十分便利,最近连掉头发现象也好了很多,于是从内心感叹 Java阵营程序员真的比 C阵营程序员工作起来舒服多了,原因就在于Java领域繁荣的生态圈催生了一大批诸如 Spring Boot这样优秀的框架的出现。这段时间也陆陆续续记录了一些有关 Spring Boot应用层开发的点点滴滴,特在此汇聚成文章合集,并 放在了Github上,项目名为 Spring-Boot-In-Action,后续仍然会持续更新。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站数据库/缓存相关Guava Cache本地缓存在 Spring Boot应用中的实践EVCache缓存在 Spring Boot中的实战Spring Boot应用缓存实践之:Ehcache加持Spring Boot集成 MyBatis和 SQL Server实践Elasticsearch搜索引擎在Spring Boot中的实践日志相关Spring Boot日志框架实践应用监控相关利用神器 BTrace 追踪线上 Spring Boot应用运行时信息Spring Boot应用监控实战Spring Boot Admin 2.0开箱体验内部机制相关SpringBoot 中 @SpringBootApplication注解背后的三体结构探秘SpringBoot 应用程序启动过程探秘如何自制一个Spring Boot Starter并推送到远端公服实战经验相关Spring Boot工程集成全局唯一ID生成器 UidGeneratorSpring Boot 工程集成全局唯一ID生成器 VestaSpring Boot优雅编码之:Lombok加持Spring Boot应用 Docker化Spring Boot热部署加持基于Spring Boot实现图片上传/加水印一把梭操作从Spring Boot到 SpringMVC自然语言处理工具包 HanLP在 Spring Boot中的应用Spring Boot应用部署于外置Tomcat容器初探 Kotlin + Spring Boot联合编程【持续更新中……】后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

January 31, 2019 · 1 min · jiezi

springboot 默认日志配置

springboot 默认日志配置SpringBoot 日志配置 默认采用LogBack作为日志输出!日志格式化具体输出的格式详解如下:2019-01-10 17:30:08.685 :日期精确到时间毫秒级别info是日志级别 : 可以设置为其他的级别如debug,error等9184 :进程id— : 分割符main: 表示主线程com.xxxxx: 通常为源码类“:” 后即为详细的日志信息控制台输出级别在application.properties文件中配置如果你的终端支持ANSI,设置彩色输出会让日志更具可读性。通过在application.properties中设置spring.output.ansi.enabled参数来支持。NEVER:禁用ANSI-colored输出(默认项)DETECT:会检查终端是否支持ANSI,是的话就采用彩色输出(推荐项)ALWAYS:总是使用ANSI-colored格式输出,若终端不支持的时候,会有很多干扰信息,不推荐使用#多彩输出spring.output.ansi.enabled=detect#日志级别logging.level.root=info#所有包下面都以debug级别输出logging.level.*=info默认输出格式可以通过 logging.pattern.console = 进行配置文件输出springboot默认会将日志输出到控制台,线上查看日志时会很不方便,一般我们都是输出到文件。需要在application.properties配置#日志输出路径问价 优先输出 logging.filelogging.file=C:/Users/tizzy/Desktop/img/my.log#设置目录,会在该目录下创建spring.log文件,并写入日志内容,logging.path=C:/Users/tizzy/Desktop/img/#日志大小 默认10MB会截断,重新输出到下一个文件中,默认级别为:ERROR、WARN、INFOlogging.file.max-size=10MBlogging.file 和 logging.path 同时设置时候会优先使用logging.file 作为日志输出。自定义日志配置日志服务在ApplicationContext 创建之前就被初始化了,并不是采用Spring的配置文件进行控制。那我们来如何进行自定义配置日志呢。springboot为我们提供了一个规则,按照规则组织配置文件名,就可以被正确加载:Logback:logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovyLog4j:log4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xmlLog4j2:log4j2-spring.xml, log4j2.xmlJDK (Java Util Logging):logging.propertiesLogBack xml配置<?xml version=“1.0” encoding=“UTF-8”?><configuration> <!– 控制台打印日志的相关配置 –> <appender name=“STDOUT” class=“ch.qos.logback.core.ConsoleAppender”> <!– 日志格式 –> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%level] - %m%n</pattern> </encoder> <!– 日志级别过滤器 –> <filter class=“ch.qos.logback.classic.filter.LevelFilter”> <!– 过滤的级别 –> <level>WARN</level> <!– 匹配时的操作:接收(记录) –> <onMatch>ACCEPT</onMatch> <!– 不匹配时的操作:拒绝(不记录) –> <onMismatch>DENY</onMismatch> </filter> </appender> <!– 文件保存日志的相关配置 –> <appender name=“ERROR-OUT” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <!– 保存日志文件的路径 –> <file>/logs/error.log</file> <!– 日志格式 –> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern> </encoder> <!– 日志级别过滤器 –> <filter class=“ch.qos.logback.classic.filter.LevelFilter”> <!– 过滤的级别 –> <level>ERROR</level> <!– 匹配时的操作:接收(记录) –> <onMatch>ACCEPT</onMatch> <!– 不匹配时的操作:拒绝(不记录) –> <onMismatch>DENY</onMismatch> </filter> <!– 循环政策:基于时间创建日志文件 –> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <!– 日志文件名格式 –> <fileNamePattern>error.%d{yyyy-MM-dd}.log</fileNamePattern> <!– 最大保存时间:30天–> <maxHistory>30</maxHistory> </rollingPolicy> </appender> <!– 基于dubug处理日志:具体控制台或者文件对日志级别的处理还要看所在appender配置的filter,如果没有配置filter,则使用root配置 –> <root level=“debug”> <appender-ref ref=“STDOUT” /> <appender-ref ref=“ERROR-OUT” /> </root></configuration>Log4j引如Log4j日志时候 需要 排除logBack日志<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j</artifactId></dependency># 日志级别,日志追加程序列表…log4j.rootLogger=DEBUG,ServerDailyRollingFile,stdout#文件保存日志log4j.appender.ServerDailyRollingFile=org.apache.log4j.DailyRollingFileAppender#文件保存日志日期格式log4j.appender.ServerDailyRollingFile.DatePattern=’.‘yyyy-MM-dd_HH#文件保存日志文件路径log4j.appender.ServerDailyRollingFile.File=/mnt/lunqi/demo/log4j.log#文件保存日志布局程序log4j.appender.ServerDailyRollingFile.layout=org.apache.log4j.PatternLayout#文件保存日志布局格式log4j.appender.ServerDailyRollingFile.layout.ConversionPattern=%d - %m%n#文件保存日志需要向后追加(false是测试的时候日志文件就清空,true的话就是在之前基础上往后写)log4j.appender.ServerDailyRollingFile.Append=false#控制台日志log4j.appender.stdout=org.apache.log4j.ConsoleAppender#控制台日志布局程序log4j.appender.stdout.layout=org.apache.log4j.PatternLayout#控制台日志布局格式log4j.appender.stdout.layout.ConversionPattern=%d yyyy-MM-dd HH:mm:ss %p [%c] %m%nLog4j2同样排除LogBack干扰,并且引入Log4j2依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency>log4j2.xml<?xml version=“1.0” encoding=“UTF-8”?><configuration> <appenders> <Console name=“Console” target=“SYSTEM_OUT”> <ThresholdFilter level=“trace” onMatch=“ACCEPT” onMismatch=“DENY” /> <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n" /> </Console> <File name=“log” fileName=“log/test.log” append=“false”> <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n" /> </File> <RollingFile name=“RollingFile” fileName=“logs/spring.log” filePattern=“log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log”> <PatternLayout pattern="%d{yyyy-MM-dd ‘at’ HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n" /> <SizeBasedTriggeringPolicy size=“50MB” /> </RollingFile> </appenders> <loggers> <root level=“trace”> <appender-ref ref=“RollingFile” /> <appender-ref ref=“Console” /> </root> </loggers></configuration>更多查看官方文档https://docs.spring.io/spring… ...

January 30, 2019 · 2 min · jiezi

spring boot 自定义规则访问获取内部或者外部静态资源图片

项目中需要将图片放在磁盘上,不能将图片放在webapp下面!springboot默认配置基本上可以满足我们的日常需要但是项目中大量用户上传的图片,不能放在tomcat下面,这样子每次重新部署项目的时候,图片就失效了,很是麻烦。所以此时就需要自定义配置springboot的项目静态文件映射springboot默认的配置规则映射 /** 到classpath:/staticclasspath:/publicclasspath:/resourcesclasspath:/META-INF/resources到本地文件路径也就是 resource/static/ 下面 访问时可以: localhost:8080/+资源路径+资源名例如我的项目结构!此时我访问的静态资源为:localhost:8080/js/jquery.min.js如果配置 jquery.min.js 直接在static下面 访问则是localhost:8080/jquery.min.js但现在需要自定义映射规则:有两种方法一种是基于配置文件,另一种是基于代码层面配置。1 基于配置文件#配置内部访问地址和外部图片访问地址 /myimgs/spring.mvc.static-path-pattern=/spring.resources.static-locations=file:C:/Users/tizzy/Desktop/img/,classpath:/static/映射 / 到 本地磁盘路径下存放的图片,和tomcat中的图片路径访问路径则是 localhost:8080/jquery.min.js localhost:8080/ 图片名2 基于代码层面配置@Configurationpublic class WebMvcConfiguration extends WebMvcConfigurerAdapter {@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //addResourceHandler是指你想在url请求的路径 //addResourceLocations是图片存放的真实路径 registry.addResourceHandler("/").addResourceLocations(“file:C:/Users/tizzy/Desktop/img/”).addResourceLocations(“classpath:/static/”); super.addResourceHandlers(registry); }}

January 30, 2019 · 1 min · jiezi

SpringBoot使用编程方式配置DataSource

Spring Boot使用固定算法来扫描和配置DataSource。这使我们可以在默认情况下轻松获得完全配置的DataSource实现。Spring Boot还会按顺序快速的自动配置连接池(HikariCP, Apache Tomcat或Commons DBCP),具体取决于路径中的哪些类。虽然Spring Boot的DataSource自动配置在大多数情况下运行良好,但有时我们需要更高级别的控制,因此我们必须设置自己的DataSource实现,因此忽略自动配置过程。Maven依赖总体而言,以编程方式创建DataSource实现非常简单。为了学习如何实现这一目标,我们将实现一个简单的存储库层,它将对某些JPA实体执行CRUD操作。我们来看看我们的演示项目的依赖项:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.4.1</version> <scope>runtime</scope> </dependency>我们将使用内存中的H2数据库实例来运行存储库层。通过这样做,我们将能够测试以编程方式配置的DataSource,而无需执行昂贵的数据库操作。让我们确保在Maven Central上查看最新版本的spring-boot-starter-data-jpa。配置DataSource如果我们坚持使用Spring Boot的DataSource自动配置并以当前状态运行我们的项目,程序将按预期工作。Spring Boot将为我们完成所有重型基础设施管道。这包括创建H2 DataSource实现,该实现将由HikariCP,Apache Tomcat或Commons DBCP自动处理,并设置内存数据库实例。此外,我们甚至不需要创建application.properties文件,因为Spring Boot也会提供一些默认的数据库设置。正如我们之前提到的,有时我们需要更高级别的自定义,因此我们必须以编程方式配置我们自己的DataSource实现。实现此目的的最简单方法是定义DataSource工厂方法,并将其放在使用@Configuration注解的类中:@Configurationpublic class DataSourceConfig { @Bean public DataSource getDataSource() { DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); dataSourceBuilder.driverClassName(“org.h2.Driver”); dataSourceBuilder.url(“jdbc:h2:mem:test”); dataSourceBuilder.username(“SA”); dataSourceBuilder.password(""); return dataSourceBuilder.build(); }}在这种情况下,我们使用方便的DataSourceBuilder类 - 一个简洁的Joshua Bloch构建器模式 - 以编程方式创建我们的自定义DataSource对象。这种方法非常好,因为构建器可以使用一些常用属性轻松配置DataSource。此外,它还可以使用底层连接池。使用application.properties文件外部化DataSource配置当然,也可以部分外部化我们的DataSource配置。例如,我们可以在工厂方法中定义一些基本的DataSource属性:@Beanpublic DataSource getDataSource() { DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); dataSourceBuilder.username(“SA”); dataSourceBuilder.password(""); return dataSourceBuilder.build(); }并在application.properties文件中指定一些额外的配置:spring.datasource.url=jdbc:h2:mem:testspring.datasource.driver-class-name=org.h2.Driver在外部源中定义的属性(例如上面的application.properties文件或通过使用@ConfigurationProperties注解的类)将覆盖Java API中定义的属性。很明显,通过这种方法,我们不再将DataSource配置设置保存在一个地方。另一方面,它允许我们保持编译时和运行时配置彼此并很好地分离。这非常好,因为它允许我们轻松设置绑定点。这样我们可以从其他来源包含不同的DataSource,而无需重构我们的bean工厂方法。测试DataSource配置测试我们的自定义DataSource配置非常简单。整个过程归结为创建JPA实体,定义基本存储库接口以及测试存储库层。创建JPA实体让我们开始定义我们的示例JPA实体类,它将为用户建模:@Entity@Table(name = “users”)public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; private String email; // standard constructors / setters / getters / toString }存储库层我们需要实现一个基本的存储库层,它允许我们对上面定义的User实体类的实例执行CRUD操作。由于我们使用的是Spring Data JPA,因此我们不必从头开始创建自己的DAO实现。我们只需要扩展CrudRepository接口获得一个工作的存储库实现:@Repositorypublic interface UserRepository extends CrudRepository<User, Long> {}测试存储库层最后,我们需要检查我们的编程配置的DataSource是否实际工作。我们可以通过集成测试轻松完成此任务:@RunWith(SpringRunner.class)@DataJpaTestpublic class UserRepositoryIntegrationTest { @Autowired private UserRepository userRepository; @Test public void whenCalledSave_thenCorrectNumberOfUsers() { userRepository.save(new User(“Bob”, “bob@domain.com”)); List<User> users = (List<User>) userRepository.findAll(); assertThat(users.size()).isEqualTo(1); } }UserRepositoryIntegrationTest类是测试用例。它只是运行两个存储库接口的CRUD方法来持久化并查找实体。请注意,无论我们是否决定以编程方式配置DataSource实现,或将其拆分为Java配置方法和application.properties文件,我们都应该始终获得有效的数据库连接。运行示例应用程序最后,我们可以使用标准的main()方法运行我们的演示应用程序:@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public CommandLineRunner run(UserRepository userRepository) throws Exception { return (String[] args) -> { User user1 = new User(“John”, “john@domain.com”); User user2 = new User(“Julie”, “julie@domain.com”); userRepository.save(user1); userRepository.save(user2); userRepository.findAll().forEach(user -> System.out.println(user); }; }}我们已经测试了存储库层,因此我们确信我们的DataSource已经成功配置。因此,如果我们运行示例应用程序,我们应该在控制台输出中看到存储在数据库中的User实体列表。 ...

January 28, 2019 · 1 min · jiezi

springboot+mybatis+mybaits plus 整合与基本应用

springboot+mybatis+mybaits plus 整合与基本应用引言在spring framework所支持的orm框架中,mybatis相比 hibernate,spring本身提供的支持是相对少的,这在开发过程中对使用mybatis进行开发的程序员来说无疑产生很多难处。为此,开源上也产生了很多三方对mybatis的一些增强工具,比如ourbatis、mybatis-generator等等。这篇我们主要来说下功能丰富、现在还在迭代的一款国人开发的增强工具mybatis-plus。就像官方文档说的那样我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。可以看出,mybatis-plus是了为了提高效率和简化配置而生的。下面就来展示下在springboot下如何整合mybatis-plus准备工作首先是创建一个springboot工程引入相关依赖(springboot相关、mybaits、mybatis-plus等等)<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– springboot对mybaits的自动配置依赖 –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </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> <!– mybatis-plus相关依赖 –> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency> </dependencies>使用mybaits-plus的代码生成器映射生成代码,我们这里所使用的数据库为mysql。这个在另外一篇文章上说明,这里就不讲述了。在这里,mybaits-plus提供了BaseMapper、BaseService这些基类来提供一些操作的支持,比如save(T t)saveOrUpdate(T t)update(T t, Wrapper<T> wrapper)page(IPage<T> page, Wrapper<T> queryWrapper)等等。下面,我们就简单介绍下springboot中怎么运用mybaits-plus配置首先是说到配置,这里默认以yml文件来进行对mybaits-plus的配置。mybatis-plus: #MyBatis Mapper 所对应的 XML 文件位置 mapper-locations: classpath*:mapper/*.xml # MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名, #注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名(即 XML 中调用的时候不用包含包名) typeAliasesPackage: com.luwei.models # 与 typeAliasesPackage 一起使用,仅扫描以该类作为父类的类 # type-aliases-super-type: java.lang.Object # 配置扫描通用枚举,配置该属性,会对枚举类进行注入 typeEnumsPackage: com.luwei.demo.mybatisplusdemo.envm # 该包下的类注册为自定义类型转换处理类,用于属性类型转换 # type-handlers-package: com.luwei.demo.mybatisplusdemo.handler # 指定 mybatis 处理器 # executorType: simple configuration: #使用驼峰法映射属性,配置这个resultType可以映射 map-underscore-to-camel-case: true global-config: db-config: # 配置表明前缀,例如表设计时表名为tb_manager,对应entity为Manager table-prefix: tb_ #逻辑已删除值 logic-delete-value: 1 #逻辑未删除值 logic-not-delete-value: 0 # 是否开启like查询,即对 stirng 字段是否使用 like,默认不开启 # column-like: false logging: level: # 日志级别,显示操作sql com.luwei.demo.mybatisplusdemo.mapper: debug基本上这些配置都能满足一般的应用了。CRUD 接口上面说到,BaseMapper和BaseService已经实现了一些基本操作,下面简单说下这些接口的用法查询查询中Mybatis-plus提供多种封装好的方式,包括对主键查询、指定条件查询、分页查询等。Manager manager1 = managerService.getById(1);Assert.assertNotNull(manager1);LambdaQueryWrapper<Manager> wrapper = new LambdaQueryWrapper<Manager>().like(Manager::getName, “na”);List<Manager> managerList = managerService.list(wrapper);Assert.assertFalse(managerList.isEmpty());//先配置page分页插件配置Page page = new Page<>(1, 2);IPage<Manager> managerPage = managerService.page(page, wrapper);Assert.assertFalse(managerPage.getRecords().isEmpty());//获取map对象Map<String, Object> map = managerService.getMap(wrapper);System.out.println(map);Object obj = managerService.getObj(wrapper);System.out.println(obj);try { //若有多个结果,抛出异常 managerService.getOne(wrapper, true);}catch (RuntimeException e) { e.printStackTrace(); System.out.println(“异常捕获”);}增加save(T t)方法,实际就是将对象持久化到数据库中,这里会产生一条insert语句,并执行。@Transactionalpublic void add() { Manager manager = new Manager(); manager.setAccount(“account”); manager.setRole(RoleEnum.ROOT); manager.setPassword(“password”); manager.setName(“name”); save(manager);}日志输出:==> Preparing: INSERT INTO tb_manager ( account, name, password, role ) VALUES ( ?, ?, ?, ? ) ==> Parameters: account(String), name(String), password(String), 0(Integer)<== Updates: 1更改提供的update、updateOrSave、upateById都可以实现数据更新。各自的区别在于update:根据条件筛选并更新指定字段updateOrSave:对对象进行更新,对未存储的对象进行插入updateById:根据id对对象进行更新@Transactionalpublic void updateManager() { Manager manager = getById(1); manager.setName(“testUpdate”); updateById(manager); //saveOrUpdate(manager); //update(new Manager(), new UpdateWrapper<Manager>().lambda().set(Manager::getName, “test”).eq(Manager::getManagerId, 1));}删除在删除中,除了一般的物理删除外,mybaits-plus还提供了逻辑删除的支持。如果需要使用逻辑删除,除了上述配置外,还需要添加一个配置bean来装配插件。@Beanpublic ISqlInjector sqlInjector() { return new LogicSqlInjector();}这样在使用删除时,会对记录中标记为删除标识的字段进行更改,在查询和更新时,也只是针对删除标识为未删除的记录。public void deleteManager() { LambdaQueryWrapper<Manager> wrapper = new LambdaQueryWrapper<Manager>().eq(Manager::getManagerId, 4); System.out.println(baseMapper.delete(wrapper)); /Map<String, Object> deleteMap = new HashMap<>(); //使用表字段名 deleteMap.put(“manager_id”, 4); baseMapper.deleteByMap(deleteMap);/ /baseMapper.deleteById(4);/ //属于service下的方法 /LambdaQueryWrapper<Manager> wrapper = new LambdaQueryWrapper<Manager>().eq(Manager::getManagerId, 4); remove(wrapper);/}条件构造器在查询、更新、删除这些操作中,我们往往需要定义条件或者设置属性,也就是where子句和set语句,如果不是直接通过sql去处理,在mybatis-plus中,也提供了一种包装器来实现。AbstractWrapper囊括了几乎满足日常需要的条件操作,和jpa的specification一样,它支持动态产生条件,也支持使用lambda表达式去组装条件。他是QueryWrapper和UpdateWrapper的父类一些常用的wrapper方法Map<String, Object> conditionMap = new HashMap<>();//使用表字段名conditionMap.put(“name”, “name”);conditionMap.put(“manager_id”, 1);//allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)//filter:忽略字段//null2IsNull:为true则在map的value为null时调用 isNull 方法,为false时则忽略value为null的QueryWrapper<Manager> queryWrapper = new QueryWrapper<Manager>().allEq((r, v) -> r.indexOf(“name”) > 0, conditionMap, true);managerService.list(queryWrapper);//like(R column, Object val) -> column like ‘%na%’//likeLeft -> column like ‘%na’//likeRight -> column like ’na%’//and(Function<This, This> func) -> and (column = val)LambdaQueryWrapper<Manager> lambdaWrapper = new LambdaQueryWrapper<Manager>().like(Manager::getName, “na”).and((r) -> r.eq(Manager::getDisabled, false));managerService.list(lambdaWrapper);//orderBy(boolean condition, boolean isAsc, R… columns) -> order by columns isAscLambdaQueryWrapper<Manager> lambdaWrapper = new LambdaQueryWrapper<Manager>().orderBy(true, false, Manager::getManagerId);managerService.list(lambdaWrapper);//select 用于挑选属性LambdaQueryWrapper<Manager> lambdaWrapper = new LambdaQueryWrapper<Manager>().select(Manager::getName, Manager::getDisabled);managerService.list(lambdaWrapper);//set(R column, Object val) -> update T set colunm = valmanagerService.update(new Manager(), new UpdateWrapper<Manager>().lambda().set(Manager::getName, “newName”).eq(Manager::getManagerId, 4));诸如还有其他像eq,lte,isNull,orderBy,or,exists等限定方法,这里就不一一介绍了。其他分页插件引入分页,除了配置上,还需要添加插件bean@Beanpublic PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor();}在使用时,自定义查询中,参数中添加Page对象,并要求放在参数中的第一位IPage<ManagerPageVO> selectManagerPage(Page page, @Param(“roleEnum”)RoleEnum roleEnum, @Param(“managerId”) Integer managerId, @Param(“name”) String name);<select id=“selectManagerPage” resultType=“com.luwei.pojos.manager.ManagerPageVO”> <![CDATA[ select manager_id, account, name, role, disabled, create_time, last_login_time from tb_manager where role = #{roleEnum} and deleted = false ]]> <if test=“name != null”> <![CDATA[ and (name like CONCAT(’%’,#{name},’%’) or account like CONCAT(’%’,#{name},’%’)) ]]> </if> <if test=“managerId != null”> <![CDATA[ and manager_id = #{managerId} ]]> </if></select><select id=“selectForSpecialCondition” resultType=“com.luwei.entity.Manager”> select <include refid=“Base_Column_List” /> from tb_manager where name like ‘%admin%’</select>主键配置在mybaits-plus中,有多种主键生成策略,它们分别是IdType.AUTO:数据库id自增IdType.INPUT:用户输入IdType.ID_WORKER:唯一ID,自动填充(默认)IdType.UUID:唯一ID,自动填充由于我们公司使用的是mysql本身的自增策略,所以选择使用 IdType.AUTO。@ApiModelProperty(value = “管理员id”)@TableId(value = “manager_id”, type = IdType.AUTO)private Integer managerId;至于 ID_WORKER 和 UUID 的不同,在于它们的唯一键生成策略不同,ID_WORKER 按照官方的介绍,是使用Sequence作为基础产生唯一键。枚举属性为了让mybaits更好地使用枚举,mybatis-plus提供了枚举扫描注入具体配置,首先是配置扫描路径# 配置扫描通用枚举,配置该属性,会对枚举类进行注入typeEnumsPackage: com.luwei.demo.mybatisplusdemo.envm在枚举类中实现接口,用于获取具体值//实现此接口用于获取值public interface BaseEnum<E extends Enum<?>, T> { T getValue(); String getDisplayName();}这样,基本上就可以在mybatis上使用枚举类型了。总结上面描述了mybitis-plus基本用法,其实除了以上,它还提供了很多方便的插件和应用,包括xml热加载、乐观锁插件、性能分析插件等,这些由于篇幅和主题的原因我就不在这里阐述了。希望这篇可以带你很快地上手这个当前热门的mybaits增强工具,提高开发效率。 ...

January 28, 2019 · 3 min · jiezi

猫头鹰的深夜翻译:Spring REST服务异常处理

前言这篇教程主要专注于如何优雅的处理WEB中的异常。虽然我们可以手动的设置ResponseStatus ,但是还有更加优雅的方式将这部分逻辑隔离开来。Spring提供了整个应用层面的异常处理的抽象,并且只是要求您添加一些注释 - 它会处理其他所有内容。下面是一些代码的示例如何手动处理异常下面的代码中, DogController将返回一个ResponseEntity实例,该实例中包含返回的数据和HttpStatus属性如果没有抛出任何异常,则下面的代码将会返回List<Dog>数据作为响应体,以及200作为状态码对于DogsNotFoundException,它返回空的响应体和404状态码对于DogServiceException, 它返回500状态码和空的响应体@RestController@RequestMapping("/dogs")public class DogsController { @Autowired private final DogsService service; @GetMapping public ResponseEntity<List<Dog>> getDogs() { List<Dog> dogs; try { dogs = service.getDogs(); } catch (DogsServiceException ex) { return new ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR); } catch (DogsNotFoundException ex) { return new ResponseEntity<>(null, null, HttpStatus.NOT_FOUND); } return new ResponseEntity<>(dogs, HttpStatus.OK); }}这种处理异常的方式最大的问题就在于代码的重复。catch部分的代码在很多其它地方也会使用到(比如删除,更新等操作)Controller AdviceSpring提供了一种更好的解决方法,也就是Controller Advice。它将处理异常的代码在应用层面上集中管理。现在我们的的DogsController的代码更加简单清晰了:import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;import static org.springframework.http.HttpStatus.NOT_FOUND;@ControllerAdvicepublic class DogsServiceErrorAdvice { @ExceptionHandler({RuntimeException.class}) public ResponseEntity<String> handleRunTimeException(RuntimeException e) { return error(INTERNAL_SERVER_ERROR, e); } @ExceptionHandler({DogsNotFoundException.class}) public ResponseEntity<String> handleNotFoundException(DogsNotFoundException e) { return error(NOT_FOUND, e); } @ExceptionHandler({DogsServiceException.class}) public ResponseEntity<String> handleDogsServiceException(DogsServiceException e){ return error(INTERNAL_SERVER_ERROR, e); } private ResponseEntity<String> error(HttpStatus status, Exception e) { log.error(“Exception : “, e); return ResponseEntity.status(status).body(e.getMessage()); }}handleRunTimeException:这个方法会处理所有的RuntimeException并返回INTERNAL_SERVER_ERROR状态码handleNotFoundException: 这个方法会处理DogsNotFoundException并返回NOT_FOUND状态码。handleDogsServiceException: 这个方法会处理DogServiceException并返回INTERNAL_SERVER_ERROR状态码这种实现的关键就在于在代码中捕获需检查异常并将其作为RuntimeException抛出。还可以用@ResponseStatus将异常映射成状态码@ControllerAdvicepublic class DogsServiceErrorAdvice { @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler({DogsNotFoundException.class}) public void handle(DogsNotFoundException e) {} @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({DogsServiceException.class, SQLException.class, NullPointerException.class}) public void handle() {} @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler({DogsServiceValidationException.class}) public void handle(DogsServiceValidationException e) {}}在自定义的异常上添加状态码@ResponseStatus(HttpStatus.NOT_FOUND)public class DogsNotFoundException extends RuntimeException { public DogsNotFoundException(String message) { super(message); }} ...

January 27, 2019 · 1 min · jiezi

[心得]SpringBoot使用addCorsMappings配置跨域的坑

什么是跨域问题这里我就不说了,直接说我使用addCorsMappings方法配置跨域时遇到的问题。具体代码如下:public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/") .allowedOrigins("") .allowedMethods(“POST”, “GET”, “PUT”, “OPTIONS”, “DELETE”) .allowCredentials(true) .allowedHeaders("") .maxAge(3600);}但是使用此方法配置之后再使用自定义拦截器时跨域相关配置就会失效。原因是请求经过的先后顺序问题,当请求到来时会先进入拦截器中,而不是进入Mapping映射中,所以返回的头信息中并没有配置的跨域信息。浏览器就会报跨域异常。正确的解决跨域问题的方法时使用CorsFilter过滤器。代码如下:private CorsConfiguration corsConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); * 请求常用的三种配置,代表允许所有,当时你也可以自定义属性(比如header只能带什么,只能是post方式等等) / corsConfiguration.addAllowedOrigin(""); corsConfiguration.addAllowedHeader(""); corsConfiguration.addAllowedMethod("*"); corsConfiguration.setAllowCredentials(true); corsConfiguration.setMaxAge(3600L); return corsConfiguration;}@Beanpublic CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/", corsConfig()); return new CorsFilter(source);}参考资料:1、【SpringMVC】与权限拦截器冲突导致的Cors跨域设置失效问题2、springboot web跨域访问问题解决–addCorsMappings和CorsFilter

January 25, 2019 · 1 min · jiezi

Spring Cloud Greenwich 新特性和F升级分享

2019.01.23 期待已久的Spring Cloud Greenwich 发布了release版本,作为我们团队也第一时间把RC版本替换为release,以下为总结,希望对你使用Spring Cloud Greenwich 有所帮助Greenwich 只支持 Spring Boot 2.1.x 分支。如果使用 2.0.x 请使用Finchley版本,pom坐标主要是适配JAVA11<!–支持Spring Boot 2.1.X–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.2.RELEASE</version> <type>pom</type> <scope>import</scope></dependency><!–Greenwich.RELEASE–><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope></dependency>升级netflix版本,DiscoveryClient支持获取InstanceIdSpring Cloud Config 提供了新的存储介质除了Git、File、JDBC,新版本提供 在Cloud Foundry的CredHub存储功能spring: profiles: active: credhub cloud: config: server: credhub: url: https://credhub:8844Spring Cloud Gateway支持整合OAuth2这里提供了一个例子: Spring Cloud Gateway and Spring Security OAuth2整合的时候有个坑可以参考这个issue:ReactiveManagementWebSecurityAutoConfiguration Prevent’s oauth2Login from being defaulted新增重写响应头过滤器spring: cloud: gateway: routes: - id: rewriteresponseheader_route uri: http://example.org filters: - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=Feign 的新特性和坑@SpringQueryMap 对Get请求进行了增强终于解决这个问题了不用直接使用OpenFeign新增的@QueryMap,由于缺少value属性 QueryMap注释与Spring不兼容…异常解决对Spring Cloud Finchley 进行直接升级时候发现feign启动报错了APPLICATION FAILED TO START***Description:The bean ‘pigx-upms-biz.FeignClientSpecification’, defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.Action:Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=trueProcess finished with exit code 1第一种粗暴的解决方法,异常日志中说明了,在 bootstrap.yml中配置spring.main.allow-bean-definition-overriding=true这是Spring Boot 2.1 后新增的属性运行bean 覆盖,不要配置到配置中心里面,不然无效第二种,就是把通过同一个服务调用的代码,移动到同一个@FeignClient中contextId ,这个是@FeignClient 新增的一个属性This will be used as the bean name instead of name if present, but will not be used as a service id.就可以用这个属性区分@FeigenClient 标志的同一个service 的接口总结Spring Cloud F – > G 变化很小,微乎其微主要是JAVA11的兼容很遗憾没有看到 Spring Cloud Alibaba 加油。Spring Cloud LoadBalancer 还是老样子。目前来看暂时无法替代 ribbon欢迎加我Q2270033969,讨论Spring Cloud ^_^ ...

January 24, 2019 · 1 min · jiezi

logback.xml日志写入数据库改造,重写源码手工读取yml参数作为数据源参数的方法

需求:实现logback日志写入数据库,并且logback关于数据库链接使用yml已有的数据源信息在logback.xml改造如下<!– 将日志存储到oracle数据库中 –> <appender name=“db-classic-oracle” class=“ch.qos.logback.classic.db.DBAppender”> <connectionSource class=“ch.qos.logback.core.db.DriverManagerConnectionSource”> </connectionSource> </appender> <!– 日志输出级别 –> <root level=“ERROR”> <appender-ref ref=“console” /> <appender-ref ref=“db-classic-oracle” /> </root> 正常上述appender部分需要设置数据源参数,类似<url>jdbc:oracle:thin:@XX:1521:orcl</url> <user>d</user> <password>111111</password> 但这部分内容实际上应用的主yml已经存在,所以想办法从yml已有的值去替换。logback本身应该能获取yml 参数。类似 <springProperty scope=“context” name=“dataUrl” source=“spring.datasource.username” defaultValue=“localhost”/>但实验了很多次,未成功,不知道为何。所以采取修改DriverManagerConnectionSource源码的方式去解决。查看源码发现下图设计的源码存在创建conn 的情况,所以已后面的代码形式去读取yml,数据库连接的相关参数即可。两种代码都能解决。//读取yml的方式1 YamlPropertiesFactoryBean yamlMapFactoryBean = new YamlPropertiesFactoryBean(); yamlMapFactoryBean.setResources(new ClassPathResource(“application.yml”)); Properties properties = yamlMapFactoryBean.getObject(); String username1=properties.getProperty(“spring.datasource.username”); //读取yml的方式2 ClassPathResource resource = new ClassPathResource(“application.yml”); InputStream inputStream = resource.getInputStream(); Map map = null; Yaml yaml = new Yaml(); map = (Map) yaml.load(inputStream);

January 24, 2019 · 1 min · jiezi

如何自制一个Spring Boot Starter并推送到远端公服

概 述传统的 Maven项目一般将需要被复用的组件做成 Module来进行管理,以便二次调用;而在 Spring Boot项目中我们则可以使用更加优雅的 Spring Boot Starter来完成这一切。基于Spring Boot开发应用的过程可谓是幸福感满满,其开箱即用的特性分析已经在 《SpringBoot 应用程序启动过程探秘》一文中详细叙述过了。这个开箱即用的魔法特性很大程度上来源于各式各样 Spring Boot Starter的加持,而且随着版本的迭代 Starter家族成员日益庞大,而且各种优秀开源作者也提供了很多非常好用的Spring Boot Starter。本文则尝试自制一个Spring Boot Starter并推送到远端仓库进行管理。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站新建项目本文准备封装一个简单的 MD5摘要工具的 Starter,命名为 md5test-spring-boot-starter,其本质就是一个 Maven项目,只不过我们需要完善pom文件的相关依赖:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency></dependencies><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>编写业务逻辑首先提供一个 MD5Util工具类,负责实际的 MD5加密:public class MD5Util { public static String getMD5( String source ) { return getMD5( source.getBytes() ); } public static String getMD5(byte[] source) { String s = null; char hexDigits[] = { ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’ }; try { java.security.MessageDigest md = java.security.MessageDigest.getInstance(“MD5”); byte tmp[]; synchronized ( MD5Util.class ) { md.update(source); tmp = md.digest(); } char str[] = new char[16 * 2]; int k = 0; for (int i = 0; i < 16; i++) { byte byte0 = tmp[i]; str[k++] = hexDigits[byte0 >>> 4 & 0xf]; str[k++] = hexDigits[byte0 & 0xf]; } s = new String(str); } catch (Exception e) { e.printStackTrace(); } return s; }}再来提供一个 MD5Service类 进行一次封装public class MD5Service { public String getMD5( String input ) { return MD5Util.getMD5( input.getBytes() ); }}编写自动装配类这一步十分重要,也是编写 Spring Boot Starter最重要的一步:@Configurationpublic class MD5AutoConfiguration { @Bean MD5Service md5Service() { return new MD5Service(); }}当然此处可以说是最简自动装配类了,该部分其实还包含各种丰富的可控注解,可以 参考这里!编写spring.factories我们还需要在 resources/META-INF/ 下创建一个名为 spring.factories的文件,然后置入以下内容:org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.codesheep.auto.MD5AutoConfiguration这一步也是相当重要哇,为什么这一步这么重要呢,因为我已经在文章《SpringBoot 应用程序启动过程探秘》 中讲过了,Spring Boot自动注入的奥秘就来源于 Spring Boot应用在启动过程中会通过 SpringFactoriesLoader 加载所有 META-INF/spring.factories 文件,通过一系列的处理流程最终将 spring.factories 文件中的定义的各种 beans 装载入 ApplicationContext容器。这一步搞定之后其实一个 Spring Boot Starter已经写好了,接下来可以通过 mvn:install打包,并传到 私有/公有Maven仓库以供使用了。推送到远端仓库很多公司都搭有私有的 Maven仓库,但个人实验可以借助于 JitPack这个 “远端公服”来为我们托管自制的 Spring Boot Starter。我们将编写好的 Spring Boot Starter代码置于 Github公有仓库上,然后通过 JitPack来拉取我们的代码并打包生成Jar包即可使用Spring Boot Starter新建一个测试工程来测试一下我们编写的 md5test-spring-boot-starter。工程创建完毕后,在 pom.xml中加入如下两个元素:添加 JitPack repository<repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository></repositories>添加 md5test-spring-boot-starter依赖:<dependency> <groupId>com.github.hansonwang99</groupId> <artifactId>md5test-spring-boot-starter</artifactId> <version>0.0.1</version></dependency>再编写一个测试 Controller来测一下MD5摘要算法的功能:@RestControllerpublic class TestController { @Autowired private MD5Service md5Service; @RequestMapping("/test") public String getMD5() { return “MD5加密结果为:” + md5Service.getMD5(“mypassword”); }}调用 /test接口后的加密结果为:MD5加密结果为:34819D7BEEABB9260A5C854BC85B3E44后记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊程序羊的 2018年终总(gen)结(feng) ...

January 24, 2019 · 2 min · jiezi

SpringBoot多环境配置self4j + logback 以及遇到的问题

SpringBoot Starter 包下面默认已经集成了Self4J + LogBack。问题大家都知道SpringBoot可以使用yml和properties两种配置方式。但是这边有一个问题,在使用yml方式配置logback的时候,不能用LogHome指定它的路径。如:logging: config: classpath: config/logback-spring:xml path: log file: loginfo<property name=“LOG_HOME” value="${LOG_PATH}/${LOG_FILE}" />这样做会导致在你的目录下生成一个loginfo的文件,而没有路径。但是如果你用properties配置文件,就不会有这个问题。我被困扰了好久···。yml多环境配置logback首先是主要的配置文件:application.ymlspring: profiles: active: dev //这边代表你需要激活哪个环境创建你需要的环境配置文件,在同一目录下:如application-dev.yml在resource根目录下创建logback配置文件<?xml version=“1.0” encoding=“UTF-8”?><configuration debug=“true”> <!– 项目名称 –> <property name=“PROJECT_NAME” value=“projectmanage”/> <!–定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径–> <property name=“LOG_HOME” value="${catalina.base:-.}/logs"/> <!– 控制台输出 –> <appender name=“CONSOLE” class=“ch.qos.logback.core.ConsoleAppender”> <withJansi>true</withJansi> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <!–格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符–> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %highlight([%-5level] %logger{50} - %msg%n)</pattern> <charset>UTF-8</charset> </encoder> </appender> <!– 系统错误日志文件 –> <appender name=“SYSTEM_FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <!– 过滤器,只打印ERROR级别的日志 –> <filter class=“ch.qos.logback.classic.filter.LevelFilter”> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class=“ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy”> <!–日志文件输出的文件名–> <FileNamePattern>${LOG_HOME}/${PROJECT_NAME}.system_error.%d{yyyy-MM-dd}.%i.log</FileNamePattern> <!–日志文件保留天数–> <MaxHistory>15</MaxHistory> <!–日志文件最大的大小–> <MaxFileSize>10MB</MaxFileSize> </rollingPolicy> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <!–格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符–> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <logger name=“system_error” additivity=“true”> <appender-ref ref=“SYSTEM_FILE”/> </logger> <!– 自己打印的日志文件,用于记录重要日志信息 –> <appender name=“MY_INFO_FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <!– 过滤器,只打印ERROR级别的日志 –> <filter class=“ch.qos.logback.classic.filter.LevelFilter”> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class=“ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy”> <!–日志文件输出的文件名–> <FileNamePattern>${LOG_HOME}/${PROJECT_NAME}.my_info.%d{yyyy-MM-dd}.%i.log</FileNamePattern> <!–日志文件保留天数–> <MaxHistory>15</MaxHistory> <!–日志文件最大的大小–> <MaxFileSize>10MB</MaxFileSize> </rollingPolicy> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <!–格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符–> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <logger name=“my_info” additivity=“true” level=“info”> <appender-ref ref=“MY_INFO_FILE”/> </logger> <!– 开发环境下的日志配置 这边就是你开发环境的配置–> <springProfile name=“dev”> <root level=“INFO”> <appender-ref ref=“CONSOLE”/> <appender-ref ref=“SYSTEM_FILE”/> </root> </springProfile> <!– 生产环境下的日志配置 这边就是你生产环境的配置–> <springProfile name=“prod”> <root level=“INFO”> <appender-ref ref=“SYSTEM_FILE”/> </root> </springProfile></configuration> ...

January 23, 2019 · 1 min · jiezi

OA管理系统 - SpringBoot + AmazeUi

本项目由 zzzmh & csyd 共同开发完成zzzmh : https://zzzmh.cn/csyd : http://csyd.xyz/前端: Amazeui + vue后端: Springboot + shiro + redis & Mysql截图展示登录首页人员信息系统监控模块系统日志建议去在线浏览一下在线浏览地址https://zzzmh.cn/projectoa/indexGitHub地址https://github.com/1812125969…Gitee码云地址https://gitee.com/tczmh/proje…DB设计书http://leanote.com/blog/post/…环境 & 框架JDK 1.8Tomcat 8.5IntelliJ IDEA 2017.3SpringBoot 1.5.10Mybatis-spring-boot-starter 1.3.1Mysql 5.7Gradle 4.4插件 & 工具Logback (spring内置) 用于记录日志文件Spring Data Redis (spring内置) 用户缓存Spring Boot ShiroSpring Boot Admin前端使用的模板AmazeUI 2.7.2prism 用于<pre>标签的代码高亮Font Awesome (amaze ui内置) 用于显示统一图标FullCalendar (amaze ui内置) 一个带备注的日历插件GitHub地址https://github.com/1812125969…其他的一些相关笔记链接毕业设计-开发避坑指南!http://leanote.com/blog/post/…DB设计http://leanote.com/blog/post/…shiro-springhttp://leanote.com/blog/post/…Spring-Data-Redishttp://leanote.com/blog/post/…彩虹猫 banner.txt for Spring Boothttp://leanote.com/blog/post/…参考http://www.ityouknow.com/spri…另外本文也发到了我的个人博客 https://zzzmh.cn/single?id=2END

January 23, 2019 · 1 min · jiezi

SpringBoot Devtools实现项目热部署

我们在开发SpringBoot项目的时候,有些时候修改了一些Controller或者Service等组件,那么每次修改都需要去重启服务,这样的话严重的导致我们的开发效率降低,那么SpringBoot为我们提供了该问题的解决方案,那就是进行热部署,我们热部署使用到的组件是devtools。修改pom文件增加maven的devtools依赖<!– 引入热部署依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional></dependency>注意:true只有设置为true时才会热启动,即当修改了html、css、js等这些静态资源后不用重启项目直接刷新即可。修改springboot插件配置<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration></plugin>配置了true后在修改java文件后也就支持了热启动,不过这种方式是属于项目重启(速度比较快的项目重启),会清空session中的值,也就是如果有用户登陆的话,项目重启后需要重新登陆。设置IDEA编辑器自动编译功能,进入IDEA的配置项中,选择顶部菜单的 IntelliJ IDEA -> Perferences… 会弹出一个设置对话框在弹出的对话框中点击Build, Execution, Deployment选项下的Compiler选项勾选Compiler选项中的Build project automatically选项开启IDEA自动编译项目,然后点击OK即可

January 23, 2019 · 1 min · jiezi

SpringBoot Environment读取配置文件乱码

今天在使用SpringBoot Environment读取配置文件中的中文时,出现了乱码问题,导致数据无法正常的显示在页面中,后经过源码的查看阅读发现是IDEA的配置问题导致的,以下是我们的具体解决思路.进入IDEA的配置项中,选择顶部菜单的 IntelliJ IDEA -> Perferences… 会弹出一个设置对话框在弹出的对话框中依次点击Editor -> File Encodings进行配置一些编码进行配置Global Encoding和Project Encoding全部设置为UTF-8的编码方式,然后去勾选最下边的Transparent native-to-ascii conversion选项配置完成点击OK重启服务即可解决乱码问题

January 23, 2019 · 1 min · jiezi

springboot 动态数据源(Mybatis+Druid)

git代码地址Spring多数据源实现的方式大概有2中,一种是新建多个MapperScan扫描不同包,另外一种则是通过继承AbstractRoutingDataSource实现动态路由。今天作者主要基于后者做的实现,且方式1的实现比较简单这里不做过多探讨。实现方式方式1的实现(核心代码):@Configuration@MapperScan(basePackages = “com.goofly.test1”, sqlSessionTemplateRef = “test1SqlSessionTemplate”)public class DataSource1Config1 { @Bean(name = “dataSource1”) @ConfigurationProperties(prefix = “spring.datasource.test1”) @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } // …..略}@Configuration@MapperScan(basePackages = “com.goofly.test2”, sqlSessionTemplateRef = “test1SqlSessionTemplate”)public class DataSourceConfig2 { @Bean(name = “dataSource2”) @ConfigurationProperties(prefix = “spring.datasource.test2”) @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } // …..略}方式2的实现(核心代码):public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private static final Logger log = Logger.getLogger(DynamicRoutingDataSource.class); @Override protected Object determineCurrentLookupKey() { //从ThreadLocal中取值 return DynamicDataSourceContextHolder.get(); }} 第1种方式虽然实现比较加单,劣势就是不同数据源的mapper文件不能在同一包名,就显得不太灵活了。所以为了更加灵活的作为一个组件的存在,作者采用的第二种方式实现。设计思路当请求经过被注解修饰的类后,此时会进入到切面逻辑中。切面逻辑会获取注解中设置的key值,然后将该值存入到ThreadLocal中执行完切面逻辑后,会执行AbstractRoutingDataSource.determineCurrentLookupKey()方法,然后从ThreadLocal中获取之前设置的key值,然后将该值返回。由于AbstractRoutingDataSource的targetDataSources是一个map,保存了数据源key和数据源的对应关系,所以能够顺利的找到该对应的数据源。源码解读org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,如下:public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); private Map<Object, DataSource> resolvedDataSources; private DataSource resolvedDefaultDataSource; protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, “DataSource router not initialized”); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException(“Cannot determine target DataSource for lookup key [” + lookupKey + “]”); } return dataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * <p>Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ protected abstract Object determineCurrentLookupKey(); //……..略targetDataSources是一个map结构,保存了key与数据源的对应关系;dataSourceLookup是一个DataSourceLookup类型,默认实现是JndiDataSourceLookup。点开该类源码会发现,它实现了通过key获取DataSource的逻辑。当然,这里可以通过setDataSourceLookup()来改变其属性,因为关于此处有一个坑,后面会讲到。public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup { public JndiDataSourceLookup() { setResourceRef(true); } @Override public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { try { return lookup(dataSourceName, DataSource.class); } catch (NamingException ex) { throw new DataSourceLookupFailureException( “Failed to look up JNDI DataSource with name ‘” + dataSourceName + “’”, ex); } }}组件使用多数据源# db1spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.master.username = rootspring.datasource.master.password = 123456spring.datasource.master.driverClassName = com.mysql.jdbc.Driverspring.datasource.master.validationQuery = truespring.datasource.master.testOnBorrow = true## db2spring.datasource.slave.url = jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.slave.username = rootspring.datasource.slave.password = 123456spring.datasource.slave.driverClassName = com.mysql.jdbc.Driverspring.datasource.slave.validationQuery = truespring.datasource.slave.testOnBorrow = true#主数据源名称spring.maindb=master#mapperper包路径mapper.basePackages =com.btps.xli.multidb.demo.mapper单数据源为了让使用者能够用最小的改动实现最好的效果,作者对单数据源的多种配置做了兼容。示例配置1(配置数据源名称):spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.master.username = rootspring.datasource.master.password = 123456spring.datasource.master.driverClassName = com.mysql.jdbc.Driverspring.datasource.master.validationQuery = truespring.datasource.master.testOnBorrow = true# mapper包路径mapper.basePackages = com.goofly.xli.multidb.demo.mapper# 主数据源名称spring.maindb=master示例配置2(不配置数据源名称):spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.username = rootspring.datasource.password = 123456spring.datasource.driverClassName = com.mysql.jdbc.Driverspring.datasource.validationQuery = truespring.datasource.testOnBorrow = true# mapper包路径mapper.basePackages = com.goofly.xli.multidb.demo.mapper踩坑之路多数据源的循环依赖Description:The dependencies of some of the beans in the application context form a cycle: happinessController (field private com.db.service.HappinessService com.db.controller.HappinessController.happinessService) ↓ happinessServiceImpl (field private com.db.mapper.MasterDao com.db.service.HappinessServiceImpl.masterDao) ↓ masterDao defined in file [E:\GitRepository\framework-gray\test-db\target\classes\com\db\mapper\MasterDao.class] ↓ sqlSessionFactory defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]┌─────┐| dynamicDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]↑ ↓| firstDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]↑ ↓| dataSourceInitializer解决方案:在Spring boot启动的时候排除DataSourceAutoConfiguration即可。如下:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})public class DBMain { public static void main(String[] args) { SpringApplication.run(DBMain.class, args); }} 但是作者在创建多数据源的时候由于并未创建多个DataSource的Bean,而是只创建了一个即需要做动态数据源的那个Bean。 其他的DataSource则直接创建实例然后存放在Map里面,然后再设置到DynamicRoutingDataSource#setTargetDataSources即可。因此这种方式也不会出现循环依赖的问题!动态刷新数据源 笔者在设计之初是想构建一个动态刷新数据源的方案,所以利用了SpringCloud的@RefreshScope去标注数据源,然后利用RefreshScope#refresh实现刷新。但是在实验的时候发现由Druid创建的数据源会因此而关闭,由Spring的DataSourceBuilder创建的数据源则不会发生任何变化。 最后关于此也没能找到解决方案。同时思考,如果只能的可以实现动态刷新的话,那么数据源的原有连接会因为刷新而中断吗还是会有其他处理?多数据源事务 有这么一种特殊情况,一个事务中调用了两个不同数据源,这个时候动态切换数据源会因此而失效。翻阅了很多文章,大概找了2中解决方案,一种是Atomikos进行事务管理,但是貌似性能并不是很理想。另外一种则是通过优先级控制,切面的的优先级必须要大于数据源的优先级,用注解@Order控制。此处留一个坑! ...

January 23, 2019 · 2 min · jiezi

Spring Data JPA REST Query Specifications

案例概述在本系列的第一篇文章中,我们将探索一种用于REST API的简单查询语言。我们将充分利用Spring作为REST API,并将JPA 2标准用于持久性方面。为什么使用查询语言?因为 - 对于任何复杂的API - 通过非常简单的字段搜索/过滤资源是不够的。查询语言更灵活,允许您精确过滤所需的资源。User Entity首先 - 让我们提出我们将用于过滤器/搜索API的简单实体 - 一个基本用户:@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age;}使用CriteriaBuilder进行过滤现在 - 让我们深入研究问题 - 持久层中的查询。构建查询抽象是一个平衡问题。一方面我们需要很大的灵活性,另一方面我们需要保持复杂性可管理性。高级别,功能很简单 - 你传递一些约束,你会得到一些结果。让我们看看它是如何工作的:@Repositorypublic class UserDAO implements IUserDAO { @PersistenceContext private EntityManager entityManager; @Override public List<User> searchUser(List<SearchCriteria> params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = builder.createQuery(User.class); Root r = query.from(User.class); Predicate predicate = builder.conjunction(); for (SearchCriteria param : params) { if (param.getOperation().equalsIgnoreCase(">")) { predicate = builder.and(predicate, builder.greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase("<")) { predicate = builder.and(predicate, builder.lessThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase(":")) { if (r.get(param.getKey()).getJavaType() == String.class) { predicate = builder.and(predicate, builder.like(r.get(param.getKey()), "%" + param.getValue() + “%”)); } else { predicate = builder.and(predicate, builder.equal(r.get(param.getKey()), param.getValue())); } } } query.where(predicate); List<User> result = entityManager.createQuery(query).getResultList(); return result; } @Override public void save(User entity) { entityManager.persist(entity); }}如您所见,searchUser API获取非常简单的约束列表,根据这些约束组成查询,执行搜索并返回结果。约束类也很简单:public class SearchCriteria { private String key; private String operation; private Object value;}该SearchCriteria实现持有我们的查询参数:key:用于保存字段名称 - 例如:firstName,age,…等。operation:用于保持操作 - 例如:Equality,less,…等。value:用于保存字段值 - 例如:john,25,…等。测试搜索查询现在 - 让我们测试我们的搜索机制,以确保它可用。首先 - 让我们通过添加两个用户来初始化我们的数据库以进行测试 - 如下例所示:@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = { PersistenceConfig.class })@Transactional@TransactionConfigurationpublic class JPACriteriaQueryTest { @Autowired private IUserDAO userApi; private User userJohn; private User userTom; @Before public void init() { userJohn = new User(); userJohn.setFirstName(“John”); userJohn.setLastName(“Doe”); userJohn.setEmail(“john@doe.com”); userJohn.setAge(22); userApi.save(userJohn); userTom = new User(); userTom.setFirstName(“Tom”); userTom.setLastName(“Doe”); userTom.setEmail(“tom@doe.com”); userTom.setAge(26); userApi.save(userTom); }}现在,让我们得到一个具有特定firstName和lastName的用户 - 如下例所示:@Testpublic void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“firstName”, “:”, “John”)); params.add(new SearchCriteria(“lastName”, “:”, “Doe”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results)));}接下来,让我们得到一个具有相同lastName的用户列表:@Testpublic void givenLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“lastName”, “:”, “Doe”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results));}接下来,让age大于或等于25的用户:@Testpublic void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“lastName”, “:”, “Doe”)); params.add(new SearchCriteria(“age”, “>”, “25”)); List<User> results = userApi.searchUser(params); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results)));}接下来,让我们搜索实际不存在的用户:@Testpublic void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“firstName”, “:”, “Adam”)); params.add(new SearchCriteria(“lastName”, “:”, “Fox”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results)));}最后,让我们搜索仅给出部分firstName的用户:@Testpublic void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“firstName”, “:”, “jo”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results)));}UserController最后,让我们现在将这种灵活搜索的持久性支持连接到我们的REST API。我们将设置一个简单的UserController - 使用findAll()使用“search”传递整个搜索/过滤器表达式:@Controllerpublic class UserController { @Autowired private IUserDao api; @RequestMapping(method = RequestMethod.GET, value = “/users”) @ResponseBody public List<User> findAll(@RequestParam(value = “search”, required = false) String search) { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); if (search != null) { Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),"); Matcher matcher = pattern.matcher(search + “,”); while (matcher.find()) { params.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); } } return api.searchUser(params); }}请注意我们如何简单地从搜索表达式中创建搜索条件对象。我们现在正处于开始使用API并确保一切正常工作的地步:http://localhost:8080/users?search=lastName:doe,age>25这是它的回应:[{ “id”:2, “firstName”:“tom”, “lastName”:“doe”, “email”:“tom@doe.com”, “age”:26}]案例结论这个简单而强大的实现支持对REST API进行相当多的智能过滤。是的—它仍然很粗糙,可以改进(下一篇文章将对此进行改进)—但它是在api上实现这种过滤功能的坚实起点。 ...

January 19, 2019 · 2 min · jiezi

Spring Boot支持Crontab任务改造

在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务。虽然 Spring 本身支持定时任务,但都是服务一直运行时支持。其实在项目中,大多数定时任务,还是借助 Linux Crontab 来支持,需要时运行即可,不需要一直占用机器资源。但 Spring Boot 项目或者普通的 jar 项目,就没这么方便了。Spring Boot 提供了类似 CommandLineRunner 的方式,很好的执行常驻任务;也可以借助 ApplicationListener 和 ContextRefreshedEvent 等事件来做很多事情。借助该容器事件,一样可以做到类似 Ant 运行的方式来运行定时任务,当然需要做一些项目改动。1. 监听目标对象借助容器刷新事件来监听目标对象即可,可以认为,定时任务其实每次只是执行一种操作而已。比如这是一个写好的例子,注意不要直接用 @Service 将其放入容器中,除非容器本身没有其它自动运行的事件。package com.github.zhgxun.learn.common.task;import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.SpringApplication;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationListener;import org.springframework.context.event.ContextRefreshedEvent;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.List;import java.util.stream.Collectors;import java.util.stream.Stream;/** * 不自动加入容器, 用于区分是否属于任务启动, 否则放入容器中, Spring 无法选择性执行 * 需要根据特殊参数在启动时注入 * 该监听器本身不能访问容器变量, 如果需要访问, 需要从上下文中获取对象实例后方可继续访问实例信息 * 如果其它类中启动了多线程, 是无法接管异常抛出的, 需要子线程中正确处理退出操作 * 该监听器最好不用直接做线程操作, 子类的实现不干预 /@Slf4jpublic class TaskApplicationListener implements ApplicationListener<ContextRefreshedEvent> { /* * 任务启动监听类标识, 启动时注入 * 即是 java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar learn.jar / private static final String SPRING_TASK_CLASS = “spring.task.class”; /* * 支持该注解的方法个数, 目前仅一个 * 可以理解为控制台一次执行一个类, 依赖的任务应该通过其它方式控制依赖 / private static final int SUPPORT_METHOD_COUNT = 1; /* * 保存当前容器运行上下文 / private ApplicationContext context; /* * 监听容器刷新事件 * * @param event 容器刷新事件 / @Override @SuppressWarnings(“unchecked”) public void onApplicationEvent(ContextRefreshedEvent event) { context = event.getApplicationContext(); // 不存在时可能为正常的容器启动运行, 无需关心 String taskClass = System.getProperty(SPRING_TASK_CLASS); log.info(“ScheduleTask spring task Class: {}”, taskClass); if (taskClass != null) { try { // 获取类字节码文件 Class clazz = findClass(taskClass); // 尝试从内容上下文中获取已加载的目标类对象实例, 这个类实例是已经加载到容器内的对象实例, 即可以获取类的信息 Object object = context.getBean(clazz); Method method = findMethod(object); log.info(“start to run task Class: {}, Method: {}”, taskClass, method.getName()); invoke(method, object); } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } finally { // 需要确保容器正常出发停止事件, 否则容器会僵尸卡死 shutdown(); } } } /* * 根据class路径名称查找类文件 * * @param clazz 类名称 * @return 类对象 * @throws ClassNotFoundException ClassNotFoundException / private Class findClass(String clazz) throws ClassNotFoundException { return Class.forName(clazz); } /* * 获取目标对象中符合条件的方法 * * @param object 目标对象实例 * @return 符合条件的方法 / private Method findMethod(Object object) { Method[] methods = object.getClass().getDeclaredMethods(); List<Method> schedules = Stream.of(methods) .filter(method -> method.isAnnotationPresent(ScheduleTask.class)) .collect(Collectors.toList()); if (schedules.size() != SUPPORT_METHOD_COUNT) { throw new IllegalStateException(“only one method should be annotated with @ScheduleTask, but found " + schedules.size()); } return schedules.get(0); } /* * 执行目标对象方法 * * @param method 目标方法 * @param object 目标对象实例 * @throws IllegalAccessException IllegalAccessException * @throws InvocationTargetException InvocationTargetException / private void invoke(Method method, Object object) throws IllegalAccessException, InvocationTargetException { method.invoke(object); } /* * 执行完毕退出运行容器, 并将返回值交给执行环节, 比如控制台等 */ private void shutdown() { log.info(“shutdown …”); System.exit(SpringApplication.exit(context)); }}其实该处仅需要启动执行即可,容器启动完毕事件也是可以的。2. 标识目标方法目标方法的标识,最方便的是使用注解标注。package com.github.zhgxun.learn.common.task.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic @interface ScheduleTask {}3. 编写任务package com.github.zhgxun.learn.task;import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;import com.github.zhgxun.learn.service.first.LaunchInfoService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service@Slf4jpublic class TestTask { @Autowired private LaunchInfoService launchInfoService; @ScheduleTask public void test() { log.info(“Start task …”); log.info(“LaunchInfoList: {}”, launchInfoService.findAll()); log.info(“模拟启动线程操作”); for (int i = 0; i < 5; i++) { new MyTask(i).start(); } try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } }}class MyTask extends Thread { private int i; private int j; private String s; public MyTask(int i) { this.i = i; } @Override public void run() { super.run(); System.out.println(“第 " + i + " 个线程启动…” + Thread.currentThread().getName()); if (i == 2) { throw new RuntimeException(“模拟运行时异常”); } if (i == 3) { // 除数不为0 int a = i / j; } // 未对字符串对象赋值, 获取长度报空指针错误 if (i == 4) { System.out.println(s.length()); } }}4. 启动改造启动时需要做一些调整,即跟普通的启动区分开。这也是为什么不要把监听目标对象直接放入容器中的原因,在这里显示添加到容器中,这样就不影响项目中类似 CommandLineRunner 的功能,毕竟这种功能是容器启动完毕就能运行的。如果要改造,会涉及到很多硬编码。package com.github.zhgxun.learn;import com.github.zhgxun.learn.common.task.TaskApplicationListener;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.builder.SpringApplicationBuilder;@SpringBootApplicationpublic class LearnApplication { public static void main(String[] args) { SpringApplicationBuilder builder = new SpringApplicationBuilder(LearnApplication.class); // 根据启动注入参数判断是否为任务动作即可, 否则不干预启动 if (System.getProperty(“spring.task.class”) != null) { builder.listeners(new TaskApplicationListener()).run(args); } else { builder.run(args); } }}5. 启动注入-Dspring.task.class 即是启动注入标识,当然这个标识不要跟默认的参数混淆,需要区分开,否则可能始终获取到系统参数,而无法获取用户参数。java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar target/learn.jar ...

January 19, 2019 · 3 min · jiezi

Spring Boot [后台脚手架] SanJi Boot v2.0 -去繁就简 重新出发

SanJi Boot v2.0去繁就简 重新出发基于Spring Boot 集成一些常用的功能,你只需要基于它做些简单的修改即可。演示环境:网址: SanJi-Boot v2.0用户名/密码: admin/admin功能列表:[x] 权限认证[x] 权限管理[x] 用户管理[x] 角色管理[x] 日志管理项目结构:sanji-boot├─java│ ├─common 公共模块│ │ ├─spring spring相关的功能│ │ └─utils 常用工具│ │ │ ├─modules 功能模块│ │ └─sys 权限模块│ │ │ └─SanjiBootApplication 项目启动类│ └─resources ├─static 第三方库、插件等静态资源 │ ├─app 项目中自己写的css js img 等资源文件 │ ├─page 页面 │ └─plugins 第三方库、插件等静态资源 │ └─application.yml 项目配置文件注意事项:运行项目前导入sanji-boot.sql技术栈(技术选型):后端:核心框架 :Spring Boot 2.1.1.RELEASE安全框架:Apache security视图框架:Spring MVC持久层框架:Spring Data JPA数据库连接池:HikariDataSource日志管理:LogBackJSON序列号框架: fastjson插件: lombok 前端:主要使用的技术:渐进式JavaScript 框架:VUE 2.2.0弹窗框架: jquery-confirm页面主体框架 :zhengAdmin效果图:源码以托管码云扩展:zhengAdmin使用VueSpring Boot 学习资料

January 18, 2019 · 1 min · jiezi

在Java中使用redisTemplate操作缓存

背景在最近的项目中,有一个需求是对一个很大的数据库进行查询,数据量大概在几千万条。但同时对查询速度的要求也比较高。这个数据库之前在没有使用Presto的情况下,使用的是Hive,使用Hive进行一个简单的查询,速度可能在几分钟。当然几分钟也并不完全是跑SQL的时间,这里面包含发请求,查询数据并且返回数据的时间的总和。但是即使这样,这样的速度明显不能满足交互式的查询需求。我们的下一个解决方案就是Presto,在使用了Presto之后,查询速度降到了秒级。但是对于一个前端查询界面的交互式查询来说,十几秒仍然是一个不能接受的时间。虽然Presto相比Hive已经快了很多(FaceBook官方宣称的是10倍),但是对分页的支持不是很友好。我在使用的时候是自己在后端实现的分页。在这种情况下应用缓存实属无奈之举。讲道理,优化应从底层开始,自底而上。上层优化的方式和效率感觉都很有局限。<!–more–>为什么要使用缓存前端查询中,单次查询的匹配数据量有可能会达到上百甚至上千条,在前端中肯定是需要分页展示的。就算每次查询10条数据,整个查询也要耗时6-8s的时间。想象一下,每翻一页等10s的场景。所以,此时使用redis缓存。减少请求数据库的次数。将匹配的数据一并存入数据库。这样只有在第一次查询时耗费长一点,一旦查询完成,用户点击下一页就是毫秒级别的操作了。使用redisTemplateSpring封装了一个比较强大的模板,也就是redisTemplate,方便在开发的时候操作Redis缓存。在Redis中可以存储String、List、Set、Hash、Zset。下面将针对List和Hash分别介绍。ListRedis中的List为简单的字符串列表,常见的有下面几种操作。hasKey判断一个键是否存在,只需要调用hasKey就可以了。假设这个Key是test,具体用法如下。if (redisTemplate.hasKey(“test”)) { System.out.println(“存在”);} else { System.out.println(“不存在”);}range该函数用于从redis缓存中获取指定区间的数据。具体用法如下。if (redisTemplate.hasKey(“test”)) { // 该键的值为 [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range(“test”, 0, 0)); // [4] System.out.println(redisTemplate.opsForList().range(“test”, 0, 1)); // [4, 3] System.out.println(redisTemplate.opsForList().range(“test”, 0, 2)); // [4, 3, 2] System.out.println(redisTemplate.opsForList().range(“test”, 0, 3)); // [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range(“test”, 0, 4)); // [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range(“test”, 0, 5)); // [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [4, 3, 2, 1] 如果结束位是-1, 则表示取所有的值}delete删除某个键。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().rightPushAll(“test”, test);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3, 4]redisTemplate.delete(“test”);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // []size获取该键的集合长度。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().rightPushAll(“test”, test);System.out.println(redisTemplate.opsForList().size(“test”)); // 4leftPush我们把存放这个值的地方想象成如图所示的容器。container并且取数据总是从左边取,但是存数据可以从左也可以从右。左就是leftPush,右就是rightPush。leftPush如下图所示。left-push用法如下。for (int i = 0; i < 4; i++) { Integer value = i + 1; redisTemplate.opsForList().leftPush(“test”, value.toString()); System.out.println(redisTemplate.opsForList().range(“test”, 0, -1));}控制台输出的结果如下。[1][2, 1][3, 2, 1][4, 3, 2, 1]leftPushAll基本和leftPush一样,只不过是一次性的将List入栈。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().leftPushAll(“test”, test);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [4, 3, 2, 1]当然你也可以这样redisTemplate.opsForList().leftPushAll(“test”, test);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [4, 3, 2, 1]leftPushIfPresent跟leftPush是同样的操作,唯一的不同是,当且仅当key存在时,才会更新key的值。如果key不存在则不会对数据进行任何操作。redisTemplate.delete(“test”);redisTemplate.opsForList().leftPushIfPresent(“test”, “1”);redisTemplate.opsForList().leftPushIfPresent(“test”, “2”);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // []leftPop该函数用于移除上面我们抽象的容器中的最左边的一个元素。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().rightPushAll(“test”, test);redisTemplate.opsForList().leftPop(“test”); // [2, 3, 4]redisTemplate.opsForList().leftPop(“test”); // [3, 4]redisTemplate.opsForList().leftPop(“test”); // [4]redisTemplate.opsForList().leftPop(“test”); // []redisTemplate.opsForList().leftPop(“test”); // []值得注意的是,当返回为空后,在redis中这个key也不复存在了。如果此时再调用leftPushIfPresent,是无法再添加数据的。有代码有真相。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().rightPushAll(“test”, test);redisTemplate.opsForList().leftPop(“test”); // [2, 3, 4]redisTemplate.opsForList().leftPop(“test”); // [3, 4]redisTemplate.opsForList().leftPop(“test”); // [4]redisTemplate.opsForList().leftPop(“test”); // []redisTemplate.opsForList().leftPop(“test”); // []redisTemplate.opsForList().leftPushIfPresent(“test”, “1”); // []redisTemplate.opsForList().leftPushIfPresent(“test”, “1”); // []rightPushrightPush如下图所示。right-push用法如下。for (int i = 0; i < 4; i++) { Integer value = i + 1; redisTemplate.opsForList().leftPush(“test”, value.toString()); System.out.println(redisTemplate.opsForList().range(“test”, 0, -1));}控制台输出的结果如下。[1][1, 2][1, 2, 3][1, 2, 3, 4]rightPushAll同rightPush,一次性将List存入。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().leftPushAll(“test”, test);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3, 4]当然你也可以这样。redisTemplate.opsForList().rightPushAll(“test”, “1”, “2”, “3”, “4”);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3, 4]rightPushIfPresent跟rightPush是同样的操作,唯一的不同是,当且仅当key存在时,才会更新key的值。如果key不存在则不会对数据进行任何操作。redisTemplate.delete(“test”);redisTemplate.opsForList().rightPushIfPresent(“test”, “1”);redisTemplate.opsForList().rightPushIfPresent(“test”, “2”);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // []rightPop该函数用于移除上面我们抽象的容器中的最右边的一个元素。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().rightPushAll(“test”, test);redisTemplate.opsForList().rightPop(“test”); // [1, 2, 3]redisTemplate.opsForList().rightPop(“test”); // [1, 2]redisTemplate.opsForList().rightPop(“test”); // [1]redisTemplate.opsForList().rightPop(“test”); // []redisTemplate.opsForList().rightPop(“test”); // []与leftPop一样,返回空之后,再调用rightPushIfPresent,是无法再添加数据的。index获取list中指定位置的元素。if (redisTemplate.hasKey(“test”)) { // 该键的值为 [1, 2, 3, 4] System.out.println(redisTemplate.opsForList().index(“test”, -1)); // 4 System.out.println(redisTemplate.opsForList().index(“test”, 0)); // 1 System.out.println(redisTemplate.opsForList().index(“test”, 1)); // 2 System.out.println(redisTemplate.opsForList().index(“test”, 2)); // 3 System.out.println(redisTemplate.opsForList().index(“test”, 3)); // 4 System.out.println(redisTemplate.opsForList().index(“test”, 4)); // null System.out.println(redisTemplate.opsForList().index(“test”, 5)); // null}值得注意的有两点。一个是如果下标是-1的话,则会返回List最后一个元素,另一个如果数组下标越界,则会返回null。trim用于截取指定区间的元素,可能你会理解成与range是一样的作用。看了下面的代码之后应该就会立刻理解。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);redisTemplate.opsForList().rightPushAll(“test”, test); // [1, 2, 3, 4]redisTemplate.opsForList().trim(“test”, 0, 2); // [1, 2, 3]其实作用完全不一样。range是获取指定区间内的数据,而trim是留下指定区间的数据,删除不在区间的所有数据。trim是void,不会返回任何数据。remove用于移除键中指定的元素。接受3个参数,分别是缓存的键名,计数事件,要移除的值。计数事件可以传入的有三个值,分别是-1、0、1。-1代表从存储容器的最右边开始,删除一个与要移除的值匹配的数据;0代表删除所有与传入值匹配的数据;1代表从存储容器的最左边开始,删除一个与要移除的值匹配的数据。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);test.add(“4”);test.add(“3”);test.add(“2”);test.add(“1”);redisTemplate.opsForList().rightPushAll(“test”, test); // [1, 2, 3, 4, 4, 3, 2, 1]// 当计数事件是-1、传入值是1时redisTemplate.opsForList().remove(“test”, -1, “1”); // [1, 2, 3, 4, 4, 3, 2]// 当计数事件是1,传入值是1时redisTemplate.opsForList().remove(“test”, 1, “1”); // [2, 3, 4, 4, 3, 2]// 当计数事件是0,传入值是4时redisTemplate.opsForList().remove(“test”, 0, “4”); // [2, 3, 3, 2]rightPopAndLeftPush该函数用于操作两个键之间的数据,接受三个参数,分别是源key、目标key。该函数会将源key进行rightPop,再将返回的值,作为输入参数,在目标key上进行leftPush。具体代码如下。List<String> test = new ArrayList<>();test.add(“1”);test.add(“2”);test.add(“3”);test.add(“4”);List<String> test2 = new ArrayList<>();test2.add(“1”);test2.add(“2”);test2.add(“3”);redisTemplate.opsForList().rightPushAll(“test”, test); // [1, 2, 3, 4]redisTemplate.opsForList().rightPushAll(“test2”, test2); // [1, 2, 3]redisTemplate.opsForList().rightPopAndLeftPush(“test”, “test2”);System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3]System.out.println(redisTemplate.opsForList().range(“test2”, 0, -1)); // [4, 1, 2, 3]Hash存储类型为hash其实很好理解。在上述的List中,一个redis的Key可以理解为一个List,而在Hash中,一个redis的Key可以理解为一个HashMap。put用于写入数据。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString()); // [1, 2, 3, 4]redisTemplate.opsForHash().put(“test”, “isAdmin”, true); // trueputALl用于一次性向一个Hash键中添加多个key。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);List<String> list2 = new ArrayList<>();list2.add(“5”);list2.add(“6”);list2.add(“7”);list2.add(“8”);Map<String, String> valueMap = new HashMap<>();valueMap.put(“map1”, list.toString());valueMap.put(“map2”, list2.toString());redisTemplate.opsForHash().putAll(“test”, valueMap); // {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]}putIfAbsent用于向一个Hash键中写入数据。当key在Hash键中已经存在时,则不会写入任何数据,只有在Hash键中不存在这个key时,才会写入数据。同时,如果连这个Hash键都不存在,redisTemplate会新建一个Hash键,再写入key。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().putIfAbsent(“test”, “map”, list.toString());System.out.println(redisTemplate.opsForHash().entries(“test”)); // {map=[1, 2, 3, 4]}get用于获取数据。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString());redisTemplate.opsForHash().put(“test”, “isAdmin”, true);System.out.println(redisTemplate.opsForHash().get(“test”, “map”)); // [1, 2, 3, 4]System.out.println(redisTemplate.opsForHash().get(“test”, “isAdmin”)); // trueBoolean bool = (Boolean) redisTemplate.opsForHash().get(“test”, “isAdmin”);System.out.println(bool); // trueString str = redisTemplate.opsForHash().get(“test”, “map”).toString();List<String> array = JSONArray.parseArray(str, String.class);System.out.println(array.size()); // 4值得注意的是,使用get函数获取的数据都是Object类型。所以需要使用类型与上述例子中的布尔类型的话,则需要强制转换一次。List类型则可以使用fastjson这种工具来进行转换。转换的例子已列举在上述代码中。delete用于删除一个Hash键中的key。可以理解为删除一个map中的某个key。 List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);List<String> list2 = new ArrayList<>();list2.add(“5”);list2.add(“6”);list2.add(“7”);list2.add(“8”);Map<String, String> valueMap = new HashMap<>();valueMap.put(“map1”, list.toString());valueMap.put(“map2”, list2.toString());redisTemplate.opsForHash().putAll(“test”, valueMap); // {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]}redisTemplate.opsForHash().delete(“test”, “map1”); // {map2=[5, 6, 7, 8]}values用于获取一个Hash类型的键的所有值。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString());redisTemplate.opsForHash().put(“test”, “isAdmin”, true);System.out.println(redisTemplate.opsForHash().values(“test”)); // [[1, 2, 3, 4], true]entries用于以Map的格式获取一个Hash键的所有值。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString());redisTemplate.opsForHash().put(“test”, “isAdmin”, true);Map<String, String> map = redisTemplate.opsForHash().entries(“test”);System.out.println(map.get(“map”)); // [1, 2, 3, 4]System.out.println(map.get(“map”) instanceof String); // trueSystem.out.println(redisTemplate.opsForHash().entries(“test”)); // {a=[1, 2, 3, 4], isAdmin=true}hasKey用于获取一个Hash键中是否含有某个键。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString());redisTemplate.opsForHash().put(“test”, “isAdmin”, true);System.out.println(redisTemplate.opsForHash().hasKey(“test”, “map”)); // trueSystem.out.println(redisTemplate.opsForHash().hasKey(“test”, “b”)); // falseSystem.out.println(redisTemplate.opsForHash().hasKey(“test”, “isAdmin”)); // truekeys用于获取一个Hash键中所有的键。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString());redisTemplate.opsForHash().put(“test”, “isAdmin”, true);System.out.println(redisTemplate.opsForHash().keys(“test”)); // [a, isAdmin]size用于获取一个Hash键中包含的键的数量。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);redisTemplate.opsForHash().put(“test”, “map”, list.toString());redisTemplate.opsForHash().put(“test”, “isAdmin”, true);System.out.println(redisTemplate.opsForHash().size(“test”)); // 2increment用于让一个Hash键中的某个key,根据传入的值进行累加。传入的数值只能是double或者long,不接受浮点型redisTemplate.opsForHash().increment(“test”, “a”, 3);redisTemplate.opsForHash().increment(“test”, “a”, -3);redisTemplate.opsForHash().increment(“test”, “a”, 1);redisTemplate.opsForHash().increment(“test”, “a”, 0);System.out.println(redisTemplate.opsForHash().entries(“test”)); // {a=1}multiGet用于批量的获取一个Hash键中多个key的值。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);List<String> list2 = new ArrayList<>();list2.add(“5”);list2.add(“6”);list2.add(“7”);list2.add(“8”);redisTemplate.opsForHash().put(“test”, “map1”, list.toString()); // [1, 2, 3, 4]redisTemplate.opsForHash().put(“test”, “map2”, list2.toString()); // [5, 6, 7, 8]List<String> keys = new ArrayList<>();keys.add(“map1”);keys.add(“map2”);System.out.println(redisTemplate.opsForHash().multiGet(“test”, keys)); // [[1, 2, 3, 4], [5, 6, 7, 8]]System.out.println(redisTemplate.opsForHash().multiGet(“test”, keys) instanceof List); // truescan获取所以匹配条件的Hash键中key的值。我查过一些资料,大部分写的是无法模糊匹配,我自己尝试了一下,其实是可以的。如下,使用scan模糊匹配hash键的key中,带SCAN的key。List<String> list = new ArrayList<>();list.add(“1”);list.add(“2”);list.add(“3”);list.add(“4”);List<String> list2 = new ArrayList<>();list2.add(“5”);list2.add(“6”);list2.add(“7”);list2.add(“8”);List<String> list3 = new ArrayList<>();list3.add(“9”);list3.add(“10”);list3.add(“11”);list3.add(“12”);Map<String, String> valueMap = new HashMap<>();valueMap.put(“map1”, list.toString());valueMap.put(“SCAN_map2”, list2.toString());valueMap.put(“map3”, list3.toString());redisTemplate.opsForHash().putAll(“test”, valueMap); // {SCAN_map2=[5, 6, 7, 8], map3=[9, 10, 11, 12], map1=[1, 2, 3, 4]}Cursor<Map.Entry<String, String>> cursor = redisTemplate.opsForHash().scan(“test”, ScanOptions.scanOptions().match("SCAN").build());if (cursor.hasNext()) { while (cursor.hasNext()) { Map.Entry<String, String> entry = cursor.next(); System.out.println(entry.getValue()); // [5, 6, 7, 8] }}引入redisTemplate如果大家看懂了怎么用,就可以将redisTemplate引入项目中了。引入pom依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.0.5.RELEASE</version></dependency>新建配置文件然后需要新建一个RedisConfig配置文件。package com.detectivehlh;import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;/** * RedisConfig * * @author Lunhao Hu * @date 2019-01-17 15:12 **/@Configurationpublic class RedisConfig { @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); //redis序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisTemplate template = new StringRedisTemplate(factory); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashKeySerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; }}注入将redisTemplate注入到需要使用的地方。@Autowiredprivate RedisTemplate redisTemplate;写在后面Github ...

January 18, 2019 · 4 min · jiezi

配置SpringBoot方便的切换jar和war

配置SpringBoot方便的切换jar和war网上关于如何切换,其实说的很明确,本文主要通过profile进行快速切换已实现在不同场合下,用不同的打包方式。jar到war修改步骤pom文件修改packaging配置由jar改为war排除tomcat等容器的依赖配置web.xml或者无web.xml打包处理入口类修改添加ServletInitializer特别注意:当改成war包的时候,application.properties配置的server.port和server.servlet.context-path就无效了,遵从war容器的安排。配置pom配置packaging<packaging>${pom.package}</packaging>修改build<!– 作用是打war包的时候,不带版本号 –><finalName>${pom.packageName}</finalName><!–加入plugin–><plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> <configuration> <!–如果想在没有web.xml文件的情况下构建WAR,请设置为false。–> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration></plugin>排除容器<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions></dependency>配置profile<profiles> <profile> <!– 开发环境 –> <id>jar</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <pom.package>jar</pom.package> <pom.packageName>${project.artifactId}-${project.version}</pom.packageName> <pom.profiles.active>dev</pom.profiles.active> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> </dependencies> </profile> <profile> <id>war</id> <properties> <pom.package>war</pom.package> <pom.packageName>${project.artifactId}</pom.packageName> <pom.profiles.active>linux</pom.profiles.active> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> </profile></profiles>修改入口类入口类继承SpringBootServletInitializer重写configure方法使用@Profile注解,当启用war配置的时候,初始化Servlet。public class Application extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Profile(value = {“war”}) @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); }} ...

January 17, 2019 · 1 min · jiezi

SpringCloud 断路器(Hystrix)

介绍雪崩效应 在微服务架构中服务与服务之间可以相互调用,由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会占用越来越多的系统资源,导致服务瘫痪。由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成影响,这就是服务故障的“雪崩”效应。断路器 “断路器”是一种开关装置,当某个服务发生故障监控(类似熔断保险丝),向调用方法返回一个备选的响应,而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。熔断模式 在对某个服务调用不可用达到一个阈值,5秒内20次调用失败就会就会启动熔断模式。熔断器的开关是由服务的健康状况(服务的健康状况 = 请求失败数 / 请求总数)决定的。当断路器开关关闭时请求可以通过,一但服务器健康状况低于设定阈值断路器就会打开,请求会被禁止通过。当断路器处于打开状态时,一段时间后会进入半开状态,这时只允许一个请求通过,如果该请求成功熔断器恢复到关闭状态,如果调用失败断路器继续保持打开。服务降级 服务降级,一般是从整体符合考虑,就是当某个服务熔断之后,服务器将不再被调用,此刻客户端可以自己准备一个本地的fallback回调,返回一个缺省值。Ribbon中使用Hystrix添加maven依赖 在SpringCloud 服务消费者(RestTemplate+Ribbon)基础上对service-ribbon项目进行修改。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>修改HelloServiceRibbonSer类@Servicepublic class HelloServiceRibbonSer { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = “helloError”) public String helloServiceRibbon(String helloName) { return restTemplate.getForObject(“http://say-hello/sayHello?helloName="+helloName,String.class); } public String helloError(String helloName) { return “hello,"+helloName+",error!”; }} @Autowired private HelloServiceRibbonSer helloServiceRibbonSer; @GetMapping("/ribbonHello”) public String ribbonHelloCtr(@RequestParam(“helloName”)String helloName){ return helloServiceRibbonSer.helloServiceRibbon(helloName); }在helloServiceRibbon方法的头上加了@HystrixCommand(fallbackMethod = “helloError”)注解,代表对这个方法使用了Hystrix相关的功能,fallbackMethod属性是调用回调之后的处理方法。修改启动类@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient //向服务中心注册@EnableHystrix //启动Hystrixpublic class ServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(ServiceRibbonApplication.class, args); }}启动项目 启动注册中心和service-ribbon,不启动say-hello项目(服务提供者),在浏览器地址栏访问http://localhost:3333/ribbonHello?helloName=aaa发现调用了fallbackMethod中的方法。Feign中使用Hystrix修改项目 修改SpringCloud 服务消费者(Feign)项目。Feign是自带断路器的,只需要在FeignClient的接口的注解中加上fallback的指定类就行了。@FeignClient(value = “say-hello”,fallback = SayHelloFeignSerHiHystric.class)public interface SayHelloFeignSer { @RequestMapping(value = “/sayHello”,method = RequestMethod.GET) String feignSayHelloSer(@RequestParam(value = “helloName”) String helloName);}@Componentpublic class SayHelloFeignSerHiHystric implements SayHelloFeignSer{ @Override public String feignSayHelloSer(String helloName) { return “发生错误 !"+helloName; }}修改配置文件#打开断路器,D版本之后默认关闭feign.hystrix.enabled=true启动项目 启动注册中心,service-feign,不启动say-hello项目。在浏览器地址栏访问:http://localhost:4444/feignSayHello?helloName=adaad,发现调用了fallback中的方法。 fallbackFactory使用 如果要熔断的功能或者业务比较复杂可以使用一个工厂来封装,然后直接使用fallbackFactory来标注。 修改SayHelloFeignSer@FeignClient(value = “say-hello”,fallbackFactory = HystrixFallBackFactory.class/fallback = SayHelloFeignSerHiHystric.class/)public interface SayHelloFeignSer { @RequestMapping(value = “/sayHello”,method = RequestMethod.GET) String feignSayHelloSer(@RequestParam(value = “helloName”) String helloName);}public interface UserFeignWithFallBackFactoryClient extends SayHelloFeignSer {}@Componentpublic class HystrixFallBackFactory implements FallbackFactory<SayHelloFeignSer> { @Override public SayHelloFeignSer create(Throwable throwable) { return new SayHelloFeignSer(){ @Override public String feignSayHelloSer(String helloName) { return “error”; } @Override public String manyParamsSer(String userName, String userPassword) { return “error”; } @Override public Object objParamsCtr(Object user) { return “error”; } }; }} ...

January 16, 2019 · 1 min · jiezi

SpringBoot统一异常处理最佳实践

摘要: SpringBoot异常处理。原文:Spring MVC/Boot 统一异常处理最佳实践作者:赵俊前言在 Web 开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多人来说, 可能对异常处理有以下几个问题:什么时候需要捕获(try-catch)异常, 什么时候需要抛出(throws)异常到上层.在 dao 层捕获还是在 service 捕获, 还是在 controller 层捕获.抛出异常后要怎么处理. 怎么返回给页面错误信息.异常处理反例既然谈到异常, 我们先来说一下异常处理的反例, 也是很多人容易犯的错误, 这里我们同时讲到前端处理和后端处理 :捕获异常后只输出到控制台前端代码$.ajax({ type: “GET”, url: “/user/add”, dataType: “json”, success: function(data){ alert(“添加成功”); }});后端代码try { // do something} catch (Exception e) { e.printStackTrace();}这是见过最多的异常处理方式了, 如果这是一个添加商品的方法, 前台通过 ajax 发送请求到后端, 期望返回 json 信息表示添加结果. 但如果这段代码出现了异常:那么用户看到的场景就是点击了添加按钮, 但没有任何反应(其实是返回了 500 错误页面, 但这里前端没有监听 error 事件, 只监听了 success 事件. 但即使加上了error: function(data) {alert(“添加失败”);}) 又如何呢? 到底因为啥失败了呢, 用户也不得而知.后台 e.printStackTrace() 打印在控制台的日志也会在漫漫的日志中被埋没, 很可能会看不到输出的异常. 但这并不是最糟的情况, 更糟糕的事情是连 e.printStackTrace() 都没有, catch 块中是空的, 这样后端的控制台中更是什么都看不到了, 这段代码会像一个隐形的炸弹一样一直埋伏在系统中.混乱的返回方式前端代码$.ajax({ type: “GET”, url: “/goods/add”, dataType: “json”, success: function(data) { if (data.flag) { alert(“添加成功”); } else { alert(data.message); } }, error: function(data){ alert(“添加失败”); }});后端代码@RequestMapping("/goods/add")@ResponseBodypublic Map add(Goods goods) { Map map = new HashMap(); try { // do something map.put(flag, true); } catch (Exception e) { e.printStackTrace(); map.put(“flag”, false); map.put(“message”, e.getMessage()); } reutrn map;}这种方式捕获异常后, 返回了错误信息, 且前台做了一定的处理, 看起来很完善? 但用 HashMap 中的 flag 和 message 这种字符串来当键很容易处理, 例如你这里叫 message, 别人起名叫 msg, 甚至有时手抖打错了, 怎么办? 前台再改成 msg 或其他的字符?, 前端后端这样一直来回改?更有甚者在情况 A 的情况下, 返回 json, 在情况 B 的情况下, 重定向到某个页面, 这就更乱了. 对于这种不统一的结构处理起来非常麻烦.异常处理规范既然要进行统一异常处理, 那么肯定要有一个规范, 不能乱来. 这个规范包含前端和后端.不要捕获任何异常对的, 不要在业务代码中进行捕获异常, 即 dao、service、controller 层的所以异常都全部抛出到上层. 这样不会导致业务代码中的一堆 try-catch 会混乱业务代码.统一返回结果集不要使用 Map 来返回结果, Map 不易控制且容易犯错, 应该定义一个 Java 实体类. 来表示统一结果来返回, 如定义实体类:public class ResultBean<T> { private int code; private String message; private Collection<T> data; private ResultBean() { } public static ResultBean error(int code, String message) { ResultBean resultBean = new ResultBean(); resultBean.setCode(code); resultBean.setMessage(message); return resultBean; } public static ResultBean success() { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage(“success”); return resultBean; } public static <V> ResultBean<V> success(Collection<V> data) { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage(“success”); resultBean.setData(data); return resultBean; } // getter / setter 略}正常情况: 调用 ResultBean.success() 或 ResultBean.success(Collection<V> data), 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 如:@RequestMapping("/goods/add")@ResponseBodypublic ResultBean<Goods> getAllGoods() { List<Goods> goods = goodsService.findAll(); return ResultBean.success(goods);}@RequestMapping("/goods/update")@ResponseBodypublic ResultBean updateGoods(Goods goods) { goodsService.update(goods); return ResultBean.success();}一般只有查询方法需要调用 ResultBean.success(Collection<V> data) 来返回 N 条数据, 其他诸如删除, 修改等方法都应该调用 ResultBean.success(), 即在业务代码中只处理正确的功能, 不对异常做任何判断. 也不需要对 update 或 delete 的更新条数做判断(个人建议, 实际需要根据业务). 只要没有抛出异常, 我们就认为用户操作成功了. 且操作成功的提示信息在前端处理, 不要后台返回 “操作成功” 等字段.前台接受到的信息为:{ “code”: 0, “message”: “success”, “data”: [ { “name”: “商品1”, “price”: 50.00, }, { “name”: “商品2”, “price”: 99.99, } ]}抛出异常: 抛出异常后, 我们应该调用 ResultBean.error(int code, String message), 来将状态码和错误信息返回, 我们约定 code 为 0 表示操作成功, 1 或 2 等正数表示用户输入错误, -1, -2 等负数表示系统错误.前台接受到的信息为:{ “code”: -1, “message”: “XXX 参数有问题, 请重新填写”, “data”: null}前端统一处理:返回的结果集规范后, 前端就很好处理了:/** * 显示错误信息 * @param result: 错误信息 /function showError(s) { alert(s);}/* * 处理 ajax 请求结果 * @param result: ajax 返回的结果 * @param fn: 成功的处理函数 ( 传入data: fn(result.data) ) /function handlerResult(result, fn) { // 成功执行操作,失败提示原因 if (result.code == 0) { fn(result.data); } // 用户操作异常, 这里可以对 1 或 2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定. else if (result.code == 1) { showError(result.message); } // 系统异常, 这里可以对 -1 或 -2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定. else if (result.code == -1) { showError(result.message); } // 如果进行细粒度的状态码判断, 那么就应该重点注意这里没出现过的状态码. 这个判断仅建议在开发阶段保留用来发现未定义的状态码. else { showError(“出现未定义的状态码:” + result.code); }}/* * 根据 id 删除商品 */function deleteGoods(id) { $.ajax({ type: “GET”, url: “/goods/delete”, dataType: “json”, success: function(result){ handlerResult(result, deleteDone); } });}function deleteDone(data) { alert(“删除成功”);}showError 和 handlerResult 是公共方法, 分别用来显示错误和统一处理结果集.然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 deleteDone 函数, 用来处理操作成功给用户的提示信息, 正所谓各司其职, 前端负责操作成功的消息提示更合理, 而错误信息只有后台知道, 所以需要后台来返回.后端统一处理异常说了这么多, 还没讲到后端不在业务层捕获任何异常的事, 既然所有业务层都没有捕获异常, 那么所有的异常都会抛出到 Controller 层, 我们只需要用 AOP 对 Controller 层的所有方法处理即可.好在 Spring 为我们提供了一个注解, 用来统一处理异常:@ControllerAdvice@ResponseBodypublic class WebExceptionHandler { private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class); @ExceptionHandler public ResultBean unknownAccount(UnknownAccountException e) { log.error(“账号不存在”, e); return ResultBean.error(1, “账号不存在”); } @ExceptionHandler public ResultBean incorrectCredentials(IncorrectCredentialsException e) { log.error(“密码错误”, e); return ResultBean.error(-2, “密码错误”); } @ExceptionHandler public ResultBean unknownException(Exception e) { log.error(“发生了未知异常”, e); // 发送邮件通知技术人员. return ResultBean.error(-99, “系统出现错误, 请联系网站管理员!”); }}在这里统一配置需要处理的异常, 同样, 对于未知的异常, 一定要及时发现, 并进行处理. 推荐出现未知异常后发送邮件, 提示技术人员.总结总结一下统一异常处理的方法:不使用随意返回各种数据类型, 要统一返回值规范.不在业务代码中捕获任何异常, 全部交由 @ControllerAdvice 来处理.一个简单的演示项目: https://github.com/zhaojun1998/exception-handler-demo本文作者: 赵俊本文链接: http://www.zhaojun.im/springboot-exception/版权声明: 本博客所有文章除特别声明外,均采用BY-NC-SA许可协议。转载请注明出处! ...

January 16, 2019 · 3 min · jiezi

SpringCloud 服务消费者(Feign)

Feign简介Feign 是一个声明web服务客户端,这便得编写web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解。在Spring Cloud中使用Feign, 我们可以做到使用HTTP请求远程服务时能与调用本地方法一样的编码体验。Feign 采用的是基于接口的注解,Feign 整合了ribbon,具有负载均衡的能力。准备工作启动注册中心eureka-server,服务提供者say-hello。对这两个项目各自启动两个实例。创建Feign客户端1.新建一个springboot工程,取名为service-feign<?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>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>service-feign</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-feign</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2.修改配置文件server.port=4444eureka.client.service-url.defaultZone=http://server2:11112/eureka/,http://server1:11111/eureka/spring.application.name=service-feign3.修改启动类@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableFeignClientspublic class ServiceFeignApplication { public static void main(String[] args) { SpringApplication.run(ServiceFeignApplication.class, args); }}4.定义Feign接口通过@ FeignClient(“服务名”),来指定调用哪个服务。@FeignClient(value = “say-hello”)public interface SayHelloFeignSer { @RequestMapping(value = “/sayHello”,method = RequestMethod.GET) String feignSayHelloSer(@RequestParam(value = “helloName”) String helloName);}5.创建controller,对外提供接口@RestControllerpublic class FeignServiceController { //编译器报错,无视。 因为这个Bean是在程序启动的时候注入的,编译器感知不到,所以报错。 @Autowired private SayHelloFeignSer sayHelloFeignSer; @GetMapping("/feignSayHello”) public String feignSayHelloCtr(@RequestParam(“helloName”)String helloName){ return sayHelloFeignSer.feignSayHelloSer(helloName); }}6.启动service-feign访问http://localhost:4444/feignSayHello?helloName=adaad,发现浏览器交替显示端口,说明feign已经集成ribbon。Feign多参数请求1.修改say-hello项目,在SayHelloController中添加两个方法/** * get请求多请求参数 * @param userName * @param userPassword * @return / @RequestMapping(value = “/manyParams”,method = RequestMethod.GET) public String manyParamsCtr(@RequestParam(“userName”)String userName,@RequestParam(“userPassword”)String userPassword){ return “用户名:"+userName+",用户密码”+userPassword; } /* * post 对象参数 * @param user * @return / @RequestMapping(value = “/objParams”,method = RequestMethod.POST) public User objParamsCtr(@RequestBody User user){ System.out.println(JSON.toJSON(user)); return user; }public class User { private String userName; private String userPassword; private String userSex; …get(),set()}2.修改service-feign项目Feign接口/* * 多参数get请求 * @param userName * @param userPassword * @return / @RequestMapping(value = “/manyParams”,method = RequestMethod.GET) String manyParamsSer(@RequestParam(“userName”)String userName,@RequestParam(“userPassword”)String userPassword); /* * 对象参数 post请求 * @param user * @return */ @RequestMapping(value = “/objParams”,method = RequestMethod.POST) Object objParamsCtr(@RequestBody Object user);3.修改service-feign项目FeignServiceController @GetMapping("/feignManyParams”) public String feignManyParamsCtr(@RequestParam(“userName”)String userName,@RequestParam(“userPassword”)String userPassword){ return sayHelloFeignSer.manyParamsSer(userName,userPassword); } @PostMapping("/feignObjParam”) public Object feignObjParamCtr(@RequestBody Map<String,Object> map){ return sayHelloFeignSer.objParamsCtr(map); }注:为了不重复创建User实体类,这里用map去接收参数。4.重新启动say-hello,service-feign两个项目通过postman访问 /feignManyParams接口访问 /feignObjParam接口 ...

January 15, 2019 · 2 min · jiezi

Spring Boot 项目启动时初始化

简介有时我们需要在启动项目时做一些操作,比如将Mysq数据库的数据导入到Redis中。这里介绍两种简单的方法。方法1;给方法添加注解@PostContruct@Componentpublic class InitServlet { @PostContruct public void init() { // 初始化操作处理 } }方法2:实现InitializingBean接口@Componentpublic class InitServlet implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { // 初始化操作处理 } }

January 15, 2019 · 1 min · jiezi

SpringCloud Finchley Gateway 缓存请求Body和Form表单

在接入Spring-Cloud-Gateway时,可能有需求进行缓存Json-Body数据或者Form-Urlencoded数据的情况。由于Spring-Cloud-Gateway是以WebFlux为基础的响应式架构设计,所以在原有Zuul基础上迁移过来的过程中,传统的编程思路,并不适合于Reactor Stream的开发。网络上有许多缓存案例,但是在测试过程中出现各种Bug问题,在缓存Body时,需要考虑整体的响应式操作,才能更合理的缓存数据下面提供缓存Json-Body数据或者Form-Urlencoded数据的具体实现方案,该方案经测试,满足各方面需求,以及避免了网络上其他缓存方案所出现的问题定义一个GatewayContext类,用于存储请求中缓存的数据import lombok.Getter;import lombok.Setter;import lombok.ToString;import org.springframework.util.LinkedMultiValueMap;import org.springframework.util.MultiValueMap;@Getter@Setter@ToStringpublic class GatewayContext { public static final String CACHE_GATEWAY_CONTEXT = “cacheGatewayContext”; /** * cache json body / private String cacheBody; /* * cache formdata / private MultiValueMap<String, String> formData; /* * cache reqeust path / private String path;}实现GlobalFilter和Ordered接口用于缓存请求数据1 . 该示例只支持缓存下面3种MediaTypeAPPLICATION_JSON–Json数据APPLICATION_JSON_UTF8–Json数据APPLICATION_FORM_URLENCODED–FormData表单数据2 . 经验总结:在缓存Body时,不能够在Filter内部直接进行缓存,需要按照响应式的处理方式,在异步操作路途上进行缓存Body,由于Body只能读取一次,所以要读取完成后要重新封装新的request和exchange才能保证请求正常传递到下游在缓存FormData时,FormData也只能读取一次,所以在读取完毕后,需要重新封装request和exchange,这里要注意,如果对FormData内容进行了修改,则必须重新定义Header中的content-length已保证传输数据的大小一致import com.choice.cloud.architect.usergate.option.FilterOrderEnum;import com.choice.cloud.architect.usergate.support.GatewayContext;import io.netty.buffer.ByteBufAllocator;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.ByteArrayResource;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.core.io.buffer.DataBufferUtils;import org.springframework.core.io.buffer.NettyDataBufferFactory;import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.http.codec.HttpMessageReader;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpRequestDecorator;import org.springframework.util.MultiValueMap;import org.springframework.web.reactive.function.server.HandlerStrategies;import org.springframework.web.reactive.function.server.ServerRequest;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.Map;@Slf4jpublic class GatewayContextFilter implements GlobalFilter, Ordered { /* * default HttpMessageReader / private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { /* * save request path and serviceId into gateway context / ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().pathWithinApplication().value(); GatewayContext gatewayContext = new GatewayContext(); gatewayContext.getAllRequestData().addAll(request.getQueryParams()); gatewayContext.setPath(path); /* * save gateway context into exchange / exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext); HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); long contentLength = headers.getContentLength(); if(contentLength>0){ if(MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){ return readBody(exchange, chain,gatewayContext); } if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)){ return readFormData(exchange, chain,gatewayContext); } } log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}",contentType, gatewayContext); return chain.filter(exchange); } @Override public int getOrder() { return Integer.MIN_VALUE; } /* * ReadFormData * @param exchange * @param chain * @return / private Mono<Void> readFormData(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){ HttpHeaders headers = exchange.getRequest().getHeaders(); return exchange.getFormData() .doOnNext(multiValueMap -> { gatewayContext.setFormData(multiValueMap); log.debug("[GatewayContext]Read FormData:{}",multiValueMap); }) .then(Mono.defer(() -> { Charset charset = headers.getContentType().getCharset(); charset = charset == null? StandardCharsets.UTF_8:charset; String charsetName = charset.name(); MultiValueMap<String, String> formData = gatewayContext.getFormData(); /* * formData is empty just return / if(null == formData || formData.isEmpty()){ return chain.filter(exchange); } StringBuilder formDataBodyBuilder = new StringBuilder(); String entryKey; List<String> entryValue; try { /* * remove system param ,repackage form data / for (Map.Entry<String, List<String>> entry : formData.entrySet()) { entryKey = entry.getKey(); entryValue = entry.getValue(); if (entryValue.size() > 1) { for(String value : entryValue){ formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&"); } } else { formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&"); } } }catch (UnsupportedEncodingException e){ //ignore URLEncode Exception } /* * substring with the last char ‘&’ / String formDataBodyString = “”; if(formDataBodyBuilder.length()>0){ formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1); } /* * get data bytes / byte[] bodyBytes = formDataBodyString.getBytes(charset); int contentLength = bodyBytes.length; ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator( exchange.getRequest()) { /* * change content-length * @return / @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0) { httpHeaders.setContentLength(contentLength); } else { httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, “chunked”); } return httpHeaders; } /* * read bytes to Flux<Databuffer> * @return / @Override public Flux<DataBuffer> getBody() { return DataBufferUtils.read(new ByteArrayResource(bodyBytes),new NettyDataBufferFactory(ByteBufAllocator.DEFAULT),contentLength); } }; ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build(); log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString); return chain.filter(mutateExchange); })); } /* * ReadJsonBody * @param exchange * @param chain * @return / private Mono<Void> readBody(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){ /* * join the body / return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { /* * read the body Flux<Databuffer> / DataBufferUtils.retain(dataBuffer); Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); /* * repackage ServerHttpRequest / ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return cachedFlux; } }; /* * mutate exchage with new ServerHttpRequest / ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build(); /* * read body string with default messageReaders */ return ServerRequest.create(mutatedExchange, messageReaders) .bodyToMono(String.class) .doOnNext(objectValue -> { gatewayContext.setCacheBody(objectValue); log.debug("[GatewayContext]Read JsonBody:{}",objectValue); }).then(chain.filter(mutatedExchange)); }); }}在后续Filter中,可以直接从ServerExchange中获取GatewayContext,就可以获取到缓存的数据,如果需要缓存其他数据,则可以根据自己的需求,添加到GatewayContext中即可GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT); ...

January 15, 2019 · 3 min · jiezi

SpringCloud 服务消费者(RestTemplate+Ribbon)

Ribbon简介Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它有助于控制HTTP和TCP的客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等,我们也可为Ribbon实现自定义的负载均衡算法。启动注册中心和服务提供者1.启动SpringCloud 高可用服务注册中心(Eureka)搭建的注册中心和服务提供者。2.在say-hello项目中添加一个controller,对外提供服务@RestControllerpublic class SayHelloController { @Value("${server.port}") private String serverPort; @GetMapping("/sayHello") public String sayHelloCtr(@RequestParam(“helloName”)String helloName){ return “Hello “+helloName+",我的端口是: “+serverPort; }}3.然后把注册中心和服务提供者say-hello分别启动两个实例。4.查看Eureka注册中心(http://localhost:11111/,http://localhost:11112/)创建服务消费者1.创建一个module项目(service-ribbon)2.添加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> <parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>service-ribbon</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-ribbon</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <!–ribbon中使用断路器–> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>3.修改配置文件server.port=3333eureka.client.service-url.defaultZone=http://server1:11111/eureka/,http://server2:11112/eureka/spring.application.name=service-ribbon4.创建一个开启负载均衡的restRemplate@Configurationpublic class RestTemplateConfig { @Bean @LoadBalanced //表明这个restRemplate开启负载均衡的功能 public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder .setConnectTimeout(20000) .setReadTimeout(30000) .build(); }}5.添加service消费say-hello提供的服务@Servicepublic class HelloServiceRibbonSer { @Autowired RestTemplate restTemplate; public String helloServiceRibbon(String helloName) { return restTemplate.getForObject(“http://say-hello/sayHello?helloName="+helloName,String.class); } }注:getForObject中的url say-hello为say-hello项目(服务提供者)的应用名称,也就是在创建say-hello项目时在配置文件中配置的spring.application.name=say-hello。sayHello为say-hello项目中接口的地址(controller), helloName是请求参数。6.创建controller,调用service中的方法@RestControllerpublic class HelloServiceRibbonControler { @Autowired private HelloServiceRibbonSer helloServiceRibbonSer; @GetMapping("/ribbonHello”) public String ribbonHelloCtr(@RequestParam(“helloName”)String helloName){ return helloServiceRibbonSer.helloServiceRibbon(helloName); }}7.启动service-ribbon,在浏览器地址栏访问ribbon项目中的controller当一直访问http://localhost:3333/ribbonHello?helloName=aaaa时可以发现浏览器交替显示端口2222和2223,说明已经实现客户端的负载均衡,并且ribbon默认采用轮询的负载均衡算法。Ribbon负载均衡策略自定义负载均衡策略1.修改service-ribbon工程配置文件添加如下配置(使用随机方式)client.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule2.修改RestTemplateConfig配置类添加@Bean public IRule ribbonRule() { return new RandomRule();//实例化与配置文件对应的策略类 }3.修改HelloServiceRibbonControler@RestControllerpublic class HelloServiceRibbonControler { @Autowired private HelloServiceRibbonSer helloServiceRibbonSer; @Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/ribbonHello”) public String ribbonHelloCtr(@RequestParam(“helloName”)String helloName){ return helloServiceRibbonSer.helloServiceRibbon(helloName); } /** * 随机方式 * @param helloName * @return */ @GetMapping("/ribbonRandomHello”) public String ribbonRandomHelloCtr(@RequestParam(“helloName”)String helloName){ this.loadBalancerClient.choose(“CLIENT”);//随机访问策略 return helloServiceRibbonSer.helloServiceRibbon(helloName); }4.启动项目依次启动eureka注册中心,say-hello项目,service-ribbon项目,访问http://localhost:3333/ribbonRandomHello?helloName=aaaa。可以发现浏览器随机显示端口。 ...

January 15, 2019 · 1 min · jiezi

SpringBoot之MongoTemplate的查询可以怎么耍

学习一个新的数据库,一般怎么下手呢?基本的CURD没跑了,当可以熟练的增、删、改、查一个数据库时,可以说对这个数据库算是入门了,如果需要更进一步的话,就需要了解下数据库的特性,比如索引、事物、锁、分布式支持等本篇博文为mongodb的入门篇,将介绍一下基本的查询操作,在Spring中可以怎么玩原文可参看: 190113-SpringBoot高级篇MongoDB之查询基本使用姿势I. 基本使用0. 环境准备在正式开始之前,先准备好环境,搭建好工程,对于这一步的详细信息,可以参考博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用接下来,在一个集合中,准备一下数据如下,我们的基本查询范围就是这些数据1. 根据字段进行查询最常见的查询场景,比如我们根据查询user=一灰灰blog的数据,这里主要会使用Query + Criteria 来完成@Componentpublic class MongoReadWrapper { private static final String COLLECTION_NAME = “demo”; @Autowired private MongoTemplate mongoTemplate; /** * 指定field查询 / public void specialFieldQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”)); // 查询一条满足条件的数据 Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | specialFieldQueryOne: " + result); // 满足所有条件的数据 List<Map> ans = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | specialFieldQueryAll: " + ans); }}上面是一个实际的case,从中可以知道一般的查询方式为:Criteria.where(xxx).is(xxx)来指定具体的查询条件封装Query对象 new Query(criteria)借助mongoTemplate执行查询 mongoTemplate.findOne(query, resultType, collectionName)其中findOne表示只获取一条满足条件的数据;find则会将所有满足条件的返回;上面执行之后,删除结果如query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | specialFieldQueryOne: {_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | specialFieldQueryAll: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]2. and多条件查询前面是只有一个条件满足,现在如果是要求同时满足多个条件,则利用org.springframework.data.mongodb.core.query.Criteria#and来斜街多个查询条件/* * 多个查询条件同时满足 /public void andQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”).and(“age”).is(18)); Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | andQuery: " + result);}删除结果如下query: Query: { “user” : “一灰灰blog”, “age” : 18 }, Fields: { }, Sort: { } | andQuery: {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}3. or或查询and对应的就是or,多个条件中只要一个满足即可,这个与and的使用有些区别, 借助org.springframework.data.mongodb.core.query.Criteria#orOperator来实现,传参为多个Criteria对象,其中每一个表示一种查询条件/* * 或查询 /public void orQuery() { // 等同于 db.getCollection(‘demo’).find({“user”: “一灰灰blog”, $or: [{ “age”: 18}, { “sign”: {$exists: true}}]}) Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”) .orOperator(Criteria.where(“age”).is(18), Criteria.where(“sign”).exists(true))); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | orQuery: " + result); // 单独的or查询 // 等同于Query: { “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } query = new Query(new Criteria().orOperator(Criteria.where(“age”).is(18), Criteria.where(“sign”).exists(true))); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | orQuery: " + result);}执行后输出结果为query: Query: { “user” : “一灰灰blog”, “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]query: Query: { “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3b0538e3ac8e8d2d392390, user=二灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]4. in查询标准的in查询case/* * in查询 /public void inQuery() { // 相当于: Query query = new Query(Criteria.where(“age”).in(Arrays.asList(18, 20, 30))); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | inQuery: " + result);}输出query: Query: { “age” : { “$in” : [18, 20, 30] } }, Fields: { }, Sort: { } | inQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]5. 数值比较数值的比较大小,主要使用的是 get, gt, lt, let/* * 数字类型,比较查询 > /public void compareBigQuery() { // age > 18 Query query = new Query(Criteria.where(“age”).gt(18)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareBigQuery: " + result); // age >= 18 query = new Query(Criteria.where(“age”).gte(18)); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareBigQuery: " + result);}/* * 数字类型,比较查询 < /public void compareSmallQuery() { // age < 20 Query query = new Query(Criteria.where(“age”).lt(20)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareSmallQuery: " + result); // age <= 20 query = new Query(Criteria.where(“age”).lte(20)); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareSmallQuery: " + result);}输出query: Query: { “age” : { “$gt” : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]query: Query: { “age” : { “$gte” : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]query: Query: { “age” : { “$lt” : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}]query: Query: { “age” : { “$lte” : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]6. 正则查询牛逼高大上的功能/* * 正则查询 /public void regexQuery() { Query query = new Query(Criteria.where(“user”).regex("^一灰灰blog”)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | regexQuery: " + result);}输出query: Query: { “user” : { “$regex” : “^一灰灰blog”, “$options” : "” } }, Fields: { }, Sort: { } | regexQuery: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afacde3ac8e8d2d392389, user=一灰灰blog2, desc=帅气逼人的码农界老秀2}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afafbe3ac8e8d2d39238b, user=一灰灰blog4, desc=帅气逼人的码农界老秀4}, {_id=5c3afb0ae3ac8e8d2d39238c, user=一灰灰blog5, desc=帅气逼人的码农界老秀5}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]7. 查询总数统计常用,这个主要利用的是mongoTemplate.count方法/* * 查询总数 /public void countQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”)); long cnt = mongoTemplate.count(query, COLLECTION_NAME); System.out.println(“query: " + query + " | cnt " + cnt);}输出query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | cnt 58. 分组查询这个对应的是mysql中的group查询,但是在mongodb中,更多的是通过聚合查询,可以完成很多类似的操作,下面借助聚合,来看一下分组计算总数怎么玩/ * 分组查询 /public void groupQuery() { // 根据用户名进行分组统计,每个用户名对应的数量 // aggregate([ { “$group” : { “_id” : “user” , “userCount” : { “$sum” : 1}}}] ) Aggregation aggregation = Aggregation.newAggregation(Aggregation.group(“user”).count().as(“userCount”)); AggregationResults<Map> ans = mongoTemplate.aggregate(aggregation, COLLECTION_NAME, Map.class); System.out.println(“query: " + aggregation + " | groupQuery " + ans.getMappedResults());}注意下,这里用Aggregation而不是前面的Query和Criteria,输出如下query: { “aggregate” : “collection”, “pipeline” : [{ “$group” : { “_id” : “$user”, “userCount” : { “$sum” : 1 } } }] } | groupQuery [{_id=一灰灰blog, userCount=5}, {_id=一灰灰blog2, userCount=1}, {_id=一灰灰blog4, userCount=1}, {_id=二灰灰blog, userCount=1}, {_id=一灰灰blog5, userCount=1}]9. 排序sort,比较常见的了,在mongodb中有个有意思的地方在于某个字段,document中并不一定存在,这是会怎样呢?/* * 排序查询 /public void sortQuery() { // sort查询条件,需要用with来衔接 Query query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | sortQuery " + result);}输出结果如下,对于没有这个字段的document也被查出来了query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | sortQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]10. 分页数据量多的时候,分页查询比较常见,用得多就是limit和skip了/* * 分页查询 */public void pageQuery() { // limit限定查询2条 Query query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)).limit(2); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | limitPageQuery " + result); // skip()方法来跳过指定数量的数据 query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)).skip(2); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | skipPageQuery " + result);}输出结果表明,limit用来限制查询多少条数据,skip则表示跳过前面多少条数据query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | limitPageQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}]query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | skipPageQuery [{_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]11. 小结上面给出的一些常见的查询姿势,当然并不全面,比如我们如果需要查询document中的部分字段怎么办?比如document内部结果比较复杂,有内嵌的对象或者数组时,嵌套查询可以怎么玩?索引什么的又可以怎么利用起来,从而优化查询效率?如何通过传说中自动生成的_id来获取文档创建的时间戳?先留着这些疑问,后面再补上II. 其他0. 项目工程:spring-boot-demomodule: mongo-template相关博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

January 13, 2019 · 5 min · jiezi

SpringCloud 高可用服务注册中心(Eureka)

集群原理不同节点Eureka Server通过Replicate(复制)进行数据同步,服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址(即:服务应用名,spring.application.name参数配置),然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。为什么需要集群(1)由于服务消费者本地缓存了服务提供者的地址,即使Eureka Server宕机,也不会影响服务之间的调用,但是一但新服务上线,已经在缓存在本地的服务提供者不可用了,服务消费者也无法知道。(2)当有成千上万个服务向服务注册中心注册的时候,注册中心的负载是非常高的。(3)在分布式系统中,任何地方存在单点故障,整个系统就不是高可用的。搭建Eureka集群1.基于SpringCloud 服务注册与发现(Eureka)创建一个mode工程<?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>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>eureka-server3</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka-server3</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2.在resources目录下创建application.ymlspring: application: name: eureka-server3 profiles: active: server1—server: port: 11111spring: profiles: server1eureka: instance: hostname: server1 client: #register-with-eureka: false #fetch-registry: false serviceUrl: defaultZone: http://server2:11112/eureka/—server: port: 11112spring: profiles: server2eureka: instance: hostname: server2 client: #register-with-eureka: false #fetch-registry: false serviceUrl: defaultZone: http://server1:11111/eureka/3.修改host文件添加对application.yml配置文件中hostname的映射10.60.52.40 server110.60.52.40 server24.修改启动类@SpringBootApplication@EnableEurekaServerpublic class EurekaServer3Application { public static void main(String[] args) { SpringApplication.run(EurekaServer3Application.class, args); }}5.启动项目在idea中把项目的Single instance only给去掉,这样一个项目就可以启动多个实例。启动项目,在启动项目的时候回发现控制台报错,因为现在只启动了一个节点,向server2注册时连接拒绝(java.net.ConnectException: Connection refused: connect)等第二个实例启动后就不会再报错。然后修改配置文件中profiles:active: server1,修改为server2,再次启动项目。访问http://localhost:11111/访问:http://localhost:11112/可以看出两个注册中心分别注册了对方。验证高可用1.创建Eureka Client修改sayHello项目中的配置文件,向注册中心中注册。一般在eureka.client.service-url.defaultZone中会把所有注册中心的地址写上,虽然说注册中心可以通过复制的方式进行数据同步。把所有地址都写上可以保证如果第一个注册中心不可用时会选择其他的注册中心进行注册。server.port=2222spring.application.name=say-helloeureka.client.service-url.defaultZone=http://server2:11112/eureka/,http://server1:11111/eureka/2.启动client访问http://localhost:11111/访问http://localhost:11112/发现say-hello已经注册到这两个注册中心中了3.关闭eureka server1访问http://localhost:11111/访问http://localhost:11112/发现say-hello的实例还在在eureka server2中。这时候会发现eureka server2控制台一直在报错,server2开启自我保护机制。4.重启eureka server1后,控制台报错停止,eureka server2关闭自我保护。同时eureka server1会同步eureka server2的实例。 ...

January 13, 2019 · 1 min · jiezi

elasticsearch入门

这篇教程主要是对在入门的elasticsearch的一个记录。ES 集群安装安装环境基于 Dokcer ,单机安装 Docker 版集群。使用版本如下:Elasticsearch 5.3.2Kibana 5.3.2JDK 8整个安装步骤分成三部分:安装 ES 集群实例 elasticsearch001安装 ES 集群实例 elasticsearch002安装 Kibana 监控安装 ES 集群实例安装过程中镜像拉取事件过长,这里笔者将docker镜像上传到阿里的docker仓库中。安装 ES 集群实例 elasticsearch001:docker run -d -p 9200:9200 \ -p 9300:9300 \ –name elasticsearch001 -h elasticsearch001 \ -e cluster.name=lookout-es \ -e ES_JAVA_OPTS="-Xms512m -Xmx512m" \ -e xpack.security.enabled=false \ registry.cn-hangzhou.aliyuncs.com/dingwenjiang/elasticsearch:5.3.2命令解释如下:docker run: 会启动一个容器实例,如果本地没有对应的镜像会去远程registry上先下载镜像。-d: 表示容器运行在后台-p [宿主机端口]:[容器内端口]: 比如-p 9200:9200 表示把宿主机的9200端口映射到容器的9200端口–name : 设置容器别名-h : 指定容器的hostname-e: 设置环境变量。这里关闭 x-pack 的安全校验功能,防止访问认证。通过curl http://localhost:9200/_cat/health?v=pretty来验证elasticsearch001是否启动成功,如下:设置环境变量的时候,我们指定了-e cluster.name=lookout-es,用于后续关联集群用。node为1 表示只有一个实例。默认 shards 分片为主备两个。status 状态是我们要关心的,状态可能是下列三个值之一:green:所有的主分片和副本分片都已分配,集群是 100% 可用的。yellow:所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。高可用会弱化把 yellow 想象成一个需要及时调查的警告。red:至少一个主分片(以及它的全部副本)都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。也可以访问 http://localhost:9200/ ,可以看到成功运行的案例,返回的 JSON 页面。如图:继续搭建elasticsearch002:docker run -d -p 9211:9200 \ -p 9311:9300 –link elasticsearch001 \ –name elasticsearch002 \ -e cluster.name=lookout-es \ -e ES_JAVA_OPTS="-Xms512m -Xmx512m" \ -e xpack.security.enabled=false \ -e discovery.zen.ping.unicast.hosts=elasticsearch001 \ registry.cn-hangzhou.aliyuncs.com/dingwenjiang/elasticsearch:5.3.2启动elasticsearch002的时候增加了几个参数,–link [其他容器名]:[在该容器中的别名]: 添加链接到另一个容器, 在本容器 hosts 文件中加入关联容器的记录。-e: 设置环境变量。这里额外指定了 ES 集群的 cluster.name、ES 集群节点淡泊配置 discovery.zen.ping.unicast.hosts 设置为实例 elasticsearch001。再次执行curl http://localhost:9200/_cat/health?v=pretty,结果如图:对比上面检查数值可以看出,首先集群状态为 green , 所有的主分片和副本分片都已分配。你的集群是 100% 可用的。相应的 node 、shards 都增加。安装 Kibana 监控接着安装Kibana,对elasticsearch进行监控,安装命令如下:# 启动kibanadocker run -d –name kibana001 \ –link elasticsearch001 \ -e ELASTICSEARCH_URL=http://elasticsearch001:9200 \ -p 5601:5601\ registry.cn-hangzhou.aliyuncs.com/dingwenjiang/kibana:5.3.2其中-e 设置环境变量。这里额外指定了 ELASTICSEARCH_URL 为搜索实例地址。打开网页访问 127.0.0.1:5601,默认账号为 elasti,密码为 changeme。会出现如下的截图:Spring Boot 整合 Elasticsearch这里只是简单整合下,开发一个web接口,实现数据存储以及查询功能。开发的思路还是传统的三层架构,controller、service、dao,这里利用spring data来简化对es的curd操作。项目的repo地址:https://github.com/warjiang/d…整个项目的结构如下所示:入口文件为:Application类,其中也是大家熟悉的spring-boot的用法。controller主要在api包下,这里会暴露出两个API接口,分别是/api/contents用于写入内容、/api/content/search用于查询service主要在service包下,与controller对应,需要实现写入和查询两个方法dao主要在repository包下,继承ElasticsearchRepository,实现curd。这里需要注意的时候,读写的bean用的是entity包下的ContentEntity,实际上services中操作的的bean是bean包下的ContentBean。后续具体的实现在这里不再赘述。项目运行起来后,可以发送写入和查询的请求来测试功能的正确性。写入请求:可以通过curl 或者postman构造一个请求如下:POST /api/contents HTTP/1.1Host: 127.0.0.1:8080Content-Type: application/jsonCache-Control: no-cache[ { “id”:1, “title”:"《见识》", “content”:“摩根说:任意让小钱从身边溜走的人,一定留不住大钱”, “type”:1, “category”:“文学”, “read”:999, “support”:100 }, { “id”:2, “title”:"《态度》", “content”:“人类的幸福不是来自偶然的幸运,而是来自每天的小恩惠”, “type”:2, “category”:“文学”, “read”:888, “support”:88 }, { “id”:3, “title”:"《Java 编程思想》", “content”:“Java 是世界上最diao的语言”, “type”:2, “category”:“计算”, “read”:999, “support”:100 }]请求成功会返回如下所示:{ “code”: 0, “message”: “success”, “data”: true}写入成功后可以到kibana中查看写入结果,打开网页访问 localhost:5601,在 Kibana 监控中输入需要监控的 index name 为 content。如下图,取消打钩,然后进入:进入后,会得到如图所示的界面,里面罗列了该索引 content 下面所有字段:打开左侧 Discover 栏目,即可看到可视化的搜索界面及数据:随便打开一个json如下:{ “_index”: “content”, “_type”: “content”, “_id”: “2”, “_score”: 1, “_source”: { “id”: 2, “title”: “《态度》”, “content”: “人类的幸福不是来自偶然的幸运,而是来自每天的小恩惠”, “type”: 2, “category”: “文学”, “read”: 888, “support”: 88 }}_index 就是索引,用于区分文档成组,即分到一组的文档集合。索引,用于存储文档和使文档可被搜索。_type 就是类型,用于区分索引中的文档,即在索引中对数据逻辑分区。比如索引 project 的项目数据,根据项目类型 ui 项目、插画项目等进行区分。_id 是该文档的唯一标示,代码中我们一 ID 作为他的唯一标示。查询请求:可以通过curl 或者postman构造一个请求如下:POST /api/content/search HTTP/1.1Host: 127.0.0.1:8080Content-Type: application/jsonCache-Control: no-cache{ “searchContent”:“Java”, “type”:2, “pageSize”:3, “pageNumber”:0}对应结果如下:{ “code”: 0, “message”: “success”, “data”: { “pageNumber”: 0, “pageSize”: 3, “totalPage”: 1, “totalCount”: 1, “result”: [ { “id”: 3, “title”: “《Java 编程思想》”, “content”: “Java 是世界上最diao的语言”, “type”: 2, “category”: “计算”, “read”: 999, “support”: 100 } ] }}这里根据 searchContent 匹配短语 +type 匹配单个字段,一起构建了搜索语句。用于搜索出我们期待的结果,就是《Java 编程思想》。 ...

January 12, 2019 · 2 min · jiezi

是时候给大家介绍SpringBoot背后豪华的研发团队了。

摘要: SpringBoot的来龙去脉。原文:为什么说 Java 程序员到了必须掌握 Spring Boot 的时候?微信公众号:纯洁的微笑Fundebug经授权转载,版权归原作者所有。看了 Pivotal 公司的发展历史,这尼玛就是一场商业大片呀。我们刚开始学习 Spring Boot 的时候肯定都会看到这么一句话:Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。这里的 Pivotal 团队肯定就是 Spring Boot 的研发团队了,那么这个 Pivotal 团队到底是个什么来头呢?和 Spring 又有那些关系?不着急且听我慢慢道来。要说起这个 Pivotal 公司的由来,我得先从 Spring 企业的这条线来说起。Spring 的发展时间回到 2002 年,当时正是 Java EE 和 EJB 大行其道的时候,很多知名公司都是采用此技术方案进行项目开发。这时候有一个美国的小伙子认为 EJB 太过臃肿,并不是所有的项目都需要使用 EJB 这种大型框架,应该会有一种更好的方案来解决这个问题。他为了证明自己的想法是正确的,在 2002 年 10 月写了一本书《Expert One-on-One J2EE》,介绍了当时 Java 企业应用程序开发的情况,并指出了 Java EE 和 EJB 组件框架中存在的一些主要缺陷。在这本书中,他提出了一个基于普通 Java 类和依赖注入的更简单的解决方案。在书中,他展示了如何在不使用 EJB 的情况下构建高质量、可扩展的在线座位预留系统。为了构建应用程序,他编写了超过 30,000 行的基础结构代码,项目中的根包命名为 com.interface21,所以人们最初称这套开源框架为 interface21,这就是 Spring 的前身。这个小伙子是谁呢?他就是大名鼎鼎的 Rod Johnson(下图),Rod Johnson 在悉尼大学不仅获得了计算机学位,同时还获得了音乐学位,更令人吃惊的是在回到软件开发领域之前,他还获得了音乐学的博士学位。现在 Rod Johnson 已经离开了 Spring,成为了一个天使投资人,同时也是多个公司的董事,早已走上人生巅峰。在这本书发布后,一对一的 J2EE 设计和开发一炮而红。这本书免费提供的大部分基础架构代码都是高度可重用的。2003 年 Rod Johnson 和同伴在此框架的基础上开发了一个全新的框架命名为 Spring,据 Rod Johnson 介绍 Spring 是传统 J2EE 新的开始,随后 Spring 发展进入快车道。2004 年 03 月,1.0 版发布。2006 年 10 月,2.0 版发布。2007 年 11 月,更名为 SpringSource,同时发布了 Spring 2.5。2009 年 12 月,Spring 3.0 发布。2013 年 12 月,Pivotal 宣布发布 Spring 框架 4.0。2017 年 09 月,Spring 5.0 发布。网上有一张图,清晰的展示了 Spring 发展:从上面这个时间线我们可以看出 Pivotal 团队和 Spring 在 2013 年交上了线,这是为什么呢?友情提示,接下来科技行业的一系列商业并购大片即将开启。Pivotal 公司上面说的 Pivotal 团队是指 Pivotal 公司,先给大家来一段 Pivotal 公司的简介:Pivotal 成立于2013年4月,致力于“改变世界构造软件的方式(We are transforming how the world builds software)”,提供云原生应用开发 PaaS 平台及服务,帮助企业客户采用敏捷软件开发方法论,从而提高软件开发人员工作效率、减少运维成本,实现数字化转型、IT 创新,并最终实现业务创新。截至目前,财富 100 强中超过三分之一的企业使用 Pivotal 云原生平台。Pivotal 部分大型客户在采用 Pivotal 产品后,开发人员与运营人员比例可提高到 200:1,开发人员专注于编写软件代码时间增长了 50%。看了简介大家可能会有点犯迷糊,这不是一个 2013 年成立的 IT 服务公司吗,和 2002 年发展起来的 Spring 又是怎么扯上关系的呢?其实呀,要说起 Pivotal 公司的起源要追溯到 1989 年的 Pivotal Labs 实验室。Pivotal Labs 公司1989 年,Rob Mee 创立的咨询公司 Pivotal Labs,专注于快速的互联网式软件开发,即敏捷编程。创立 Pivotal Labs 的时候,它还是一家非常小的软件顾问公司,它的主营业务就是与客户合作,帮助客户开发软件。Pivotal Labs 一直是敏捷开发领域的领导者,为部分硅谷最有影响力的公司塑造了软件开发文化,并树立了良好口碑,其中 Google、Twitter 都曾是 Pivotal Labs 客户。时间很快到了 2012 年,深受客户喜爱的 Pivotal 终于引起了商用软件巨头 EMC 的关注,EMC 在 2012 年以现金方式收购了 Pivotal 并照单全收了它的 200 名员工。刚开始的时候,公司并没有发生太大的变化,只是作为新部门成为了 EMC 的一部分,Pivotal Labs 仍然继续像以前样与客户合作。但是到 2013 年的时候,EMC 突然扔下了一颗重磅炸弹。它将 Pivotal Labs 的核心业务分拆出去,成立了一家名为 Pivotal Software 的新公司。这家新公司的股东是 EMC 、 VMware 和通用电气,之前在 EMC 子公司 VMware 担任首席执行官的马瑞兹出任公司的首席执行官。EMC 和 VMware 分拆出其 Cloud Foundry、Pivotal Labs、Greenplum 等云计算、大数据资源,GE 投资 1.05 亿美元,成立新公司 Pivotal。新生的 Pivotal 是名副其实的“富二代”,这轮估值高达 10.5 亿美元。那么 EMC 和 VMware 又有什么关联呢?2003 年 12 月, EMC 公司宣布以 6.35 亿美元收购了 VMware 公司。EMC 于 1979 年成立于美国麻州 Hopkinton 市,1989 年开始进入企业数据储存市场。二十多年来,EMC 全心投注在各项新的储存技术,已获得了 1,300 个已通过或审核中的储存技术专利。无论是全球外接 RAID 储存系统、网络储存亦或是储存管理软件等储存专业领域,EMC 均是业界公认的领导厂商。EMC 是全球第六大企业软件公司,全球信息基础架构技术与解决方案的领先开发商与提供商。同时也是美国财富五百强之一,在全世界拥有超过四万二千名员工,在全球 60 个国家或地区拥有分支机构。我们接触比较多就是 EMC 的各种存储产品。EMC 公司做大 EMC 的秘诀,就是研发与并购双轮驱动,研发与并购的投入占当年营业收入的 22% 左右,并购投入略高于研发。从 2003 年到2 015 年的 12 年间,EMC 总共投入超过 420 亿美元用于研发和收购。其中,206 亿美元用于研发,213 亿美元用于并购,总共并购了 100 多家公司。VMware 收购 Spring2009 年是 Spring 企业的一个转折点,VMware 以 4.2 亿美元收购 Spring Source (3.6亿现金外加5800万股份) 。可以说虚拟化就是 VMware 发明的VMware 于 1998 年成立,公司总部位于美国加州帕洛阿尔托,是全球云基础架构和移动商务解决方案厂商,提供基于VMware的解决方案,企业通过数据中心改造和公有云整合业务,借助企业安全转型维系客户信任,实现任意云端和设备上运行、管理、连接及保护任意应用。2018 财年全年收入 79.2 亿美元。相信作为研发人员肯定都使用过 VMware 公司的产品,最常用的是 VMware 的虚拟机产品,但其实 VMware 公司的产品线非常多。从发展路线来看,VMware 具备三大特点:第一,是技术具备领先性,虚拟化技术在70年代就已出现,但VMware是第一个将这项技术应用到X86服务器上,并在这个基础上不断完善,使其能够满足企业级客户需求;第二,是瞄准大型企业客户。VMware 刚刚上市时,年营收不到4亿美金,但已经覆盖80%的财富1000强客户;第三,是高度产品化。VMware 的毛利率长期保持在 85% 左右,咨询业务占比非常少,几乎将所有部署工作都交给合作伙伴。VMware 也是一个并购大户,通过投资和收购补全业务线,客户资源是一大优势。2012 年 Rod Johnson 宣布他将要离开 Spring Source 。EMC 又被收购2015 年的时候,曾经被大量报道 EMC 考虑被子公司 VMware 收购,让人大跌眼镜,竟然可以有这样的骚动作,这是为什么呢?EMC 在 2003 年斥资 6.25 亿美元收购了 VMware,四年之后,EMC 选择让 VMware 分拆上市,结果独立上市的 VMware 发展越来越好,反观 EMC 的各项业务持续陷入低潮。到 2015 年的时候,VMware 的市值已达到约 370 亿美元,占据了 EMC 总市值的近 75%。可能各方利益不能达成一致,最终 EMC 却被戴尔(dell)收购。2015 年 10 月 12 日,戴尔(Dell)和EMC(易安信)公司宣布签署最终协议,戴尔公司与其创始人、主席和首席执行官麦克尔•戴尔,与 马上到! Partner 以及银湖资本一起,收购 EMC 公司,交易总额达 670亿 美元,成为科技史上最大并购。当时业界最关心的云计算软件商 VMware 仍然保持独立上市公司的身份。据悉,EMC 当前持有 VMware 大约 80% 的股权,市值约为 320 亿美元。而戴尔收购 EMC 实际上是项庄舞剑,VMware 才是戴尔收购 EMC 的关键。戴尔的故事1984 年,创办人迈克尔·戴尔在德州大学奥斯汀分校就学时创立了 PCs Limited 这家计算机公司。在 1985 年,公司生产了第一部拥有自己独特设计的计算机“Turbo PC”,售价为 795 美元。从此开启了戴尔公司的发展史,下面为戴尔公司的里程碑1984年 - 年仅19岁的Michael Dell凭借1,000美元的资金建立了PC’s Limited,并且树立了颠覆技术行业的愿景。1988年 - 我们完成了首次公开募股,募集了3,000万美元资金,公司市值从1,000美元增长到8500万美元。1992年 - 戴尔跻身财富500强公司行列,Michael Dell也成为榜单上最年轻的CEO。1996年 - Dell.com上线,该站点上线仅六个月之后,每天销售额即达100万美元。2001年 - 戴尔成为 全球第一大计算机系统提供商。2005年 - 在《财富》杂志的“美国最受赞赏公司”排名中,戴尔位列第一。2010年 - 戴尔被 Gartner, Inc.评为世界第一大医疗保健信息技术服务提供商。2013年 - Michael Dell携手私人股本公司Silver Lake Partners,从公众股东手里买回了戴尔股份,旨在加快解决方案战略的实施并专注于大多数客户重视的创新和长期投资。2016年 - 戴尔与EMC合并为Dell Technologies,这是业内最大的技术集成事件。戴尔提供的工作2018年的时候又传出,VMware 反收购戴尔?写到这里的时候我都感觉有点乱了?戴尔收购了 EMC, ECM 收购了 VMware ,那么 VMware 就差不多算戴尔的重孙子,那么怎么又来 VMware 反收购戴尔?原来是这样,在 2015 年 10 月 12 日业界正式爆料戴尔收购 EMC(包括 VMware),当时的 VMware 股价在 60-70 美元左右。到了 2016 年 9 月戴尔宣布正式并购 EMC 包括 VMware,只是让 VMware 独立运营,VMware 当时股价也还是在 70 美元左右。可是到了 2018 年初一看,VMware 股价已经到达了 130 多美元,在 2018 年的最高点,股价甚至达到了 160 多美元,股价又 TM 涨了一倍多,VMware 公司简直发展太好了。VMware 最新的市值快到了 6000 亿美金,当初收购时 VMware 市值也就 200 多亿美金,简直赚翻了呀!传言只是传言,最终 2018 年 7 月,戴尔还是选择了独立上市,拥有 VMware 80% 的股份。并购时间表上面写的有点乱,大家看完之后也许有点迷糊,在这里重新整理一下这里面几个关键公司的收购时间点:1989 年,Rob Mee 创立的咨询公司 Pivotal Labs;2003 年,Rod Johnson 和同伴创建了 Spring;2003 年,EMC 收购了 VMware 公司;2009 年,VMware 收购了 Spring ;2012 年,EMC 又收购了 Pivotal Labs 公司;2013 年,EMC 、 VMware 和收购来的 Pivotal Labs 公司重新组建了新的公司 Pivotal;2015 年,戴尔又并购了 EMC;2018 年,戴尔独立上市。接着说 Pivotal 公司上面一系列的商业并购搞的眼花缭乱的,但是大家只要知道 Pivotal 公司出身高贵,来自几个都不太差钱的世界 500 强公司联合组建而成,Pivotal 公司的产品非常的高大上,就连我们平时使用的 12306 都使用了他们公司的产品。Pivotal 公司可谓是大牛云集,公司的开源产品有:Spring 以及 Spring 衍生产品、Web 服务器 Tomcat、缓存中间件 Redis、消息中间件 RabbitMQ、平台即服务的 Cloud Foundry、Greenplum 数据引擎、还有大名鼎鼎的 GemFire(12306 系统解决方案组件之一)。这些著名开源产品背后的开发者都在 Pivotal 公司,其研发团队汇集了全球的一流开发者,Spring Boot 为什么如此优秀,或许在这里可以找到一些答案。Pivotal 中国研发中心在中国创建于 2010 年,它的前身是 EMC Greenplum 部门,其团队成员分布在北京和上海两地,目前正致力于以下产品的研发和服务的提供:Pivotal Web Service (PWS), Pivotal Hadoop (PHD), Hawq 和 Greenplum Database (GPDB)。毕威拓科技(北京)有限公司(Pivotal中国公司)2015年3月1日正式成立并单独运营。Pivotal 公司成立之后,于 2014 年发布了 Spring Boot,2015 年发布了 Spring Cloud,2018 年 Pivotal 公司在纽约上市。我们可以通过一张图来了解 Pivotal 公司的发展史。Pivotal 的定位是一家下一代云计算和大数据应用相结合的公司,而 VMWare 和原 EMC 的业务方向则依然是软件定义数据中心和信息基础架构。官网这样介绍他们的产品:Pivotal 提供的工具能够帮助开发人员构建更出色软件,可让您在任意云环境中运行应用的平台,帮助您打造未来。公司的产品主要分为三大类:部署和运行软件,规划、构建和集成软件,分析和决策部署和运行软件Pivotal Cloud Foundry (PCF),用于快速交付应用、容器和函数的多云平台。PCF: Pivotal Application Service,在具有内置日志记录、监控和自动扩展功能且高度可用的自助服务平台上,运行使用任意语言构建的应用。PCF: Pivotal Container Service,基于企业级Kubernetes环境构建应用,该环境采用按需群集、滚动升级和VMware NSX提供的软件定义的网络。Pivotal Services Marketplace,将您的应用与托管、代理或按需服务相结合。产品涵盖数据管理、API管理、消息传递、日志记录等。规划、构建和集成软件Spirng Boot,借助领先的Java框架快速构建功能强大的应用和服务。Spirng Cloud,将经过验证的微服务模式融入您的软件。提供配置存储、服务发现、消息传递等功能。Steeltoe,受 Spring Cloud 启发,用该框架构建恢复力强、可扩展的.NET应用。Pivotal Cloud Cache,采用基于 Pivotal GemFire 的快速且高度可用的缓存,可提供地理复制和后台写入功能。Pivotal GemFire,利用可扩展、事件驱动的分布式数据网格执行内存中计算。12306采用的商业方案。RabbitMQ,借助这款广受欢迎的消息传递代理,分离服务并扩展处理进程。Pivotal Tracker,经过验证的项目管理工具,帮您打造成功的敏捷团队。Concourse,利用自动化管道实现 PCF 的持续升级。分析和决策Pivotal Greenplum,使用这个功能齐全的多云大规模并行处理(MPP)数据平台,可以对大型数据集进行高级分析。。Apache MADlib,通过采用数据并行方式实施多结构数据的数学、统计和机器学习方法来执行数据库内分析。Pivotal 公司的产品有 Spring Boot 、Spring Cloud 、RabbitMQ 等非常著名的开源软件,也有很多类似 GemFire 等商业解决方案,通过他们公司的产品即可发现,一边通过开源软件打造生态,一方面通过商业解决方案来挣钱。曾经有一段时间,有人就问我一个问题,说开源的是不是就意味着是免费的,不免费的服务,是不是就意味着不是开源的软件?这种商业模式其实就是对这种观点的一种反驳,开源不等于免费,开源是一种开放分享的精神,不要什么东西来到国内都变味了。Pivotal 掌握很多最新前沿的开源技术,公司提供的从云端部署到一整套的大数据解决方案,从开发到平台到提供解决方案到提供咨询,可以说真正依赖技术挣钱的典范,我辈之楷模!最后送大家一个学习 Spring Boot 的开源项目: spring-boot-examples参考是时候说说Pivotal这个富二代了!五年做到60亿美金市值,PaaS第一股Pivotal的崛起之路 | 爱分析调研原文链接:https://www.cnblogs.com/ityou… ...

January 11, 2019 · 3 min · jiezi

SpringCloud 服务注册与发现(Eureka)

版本sporingboot:2.0.5springcloud:Finchley.RELEASE创建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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <name>my_cloud</name> <packaging>pom</packaging> <description>Demo project for Spring Boot</description> <modules> <module>eurekaServer</module> <module>sayHello</module> </modules> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-cloud.version>Finchley.RELEASE</spring-cloud.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-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> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories></project>创建model创建两个model工程,创建model一个作为eureka server,一个是client也就是具体的服务。创建eureka server<?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>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka-server</name> <description>Demo project for Spring Boot</description> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>配置文件:server.port=1111eureka.instance.hostname=localhost#这两个是表明自己是eureka Server#是否向服务注册中心注册自己eureka.client.register-with-eureka=false#是否检索服务eureka.client.fetch-registry=false#关闭自我保护机制(生产环境建议开启)#eureka.server.enable-self-preservation=true#服务注册中心的配置内容,指定服务注册中心的位置eureka.client.service-url.defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/spring.application.name: eurka-server在启动类上添加@EnableEurekaServer@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }}启动eureka server,在浏览器地址栏输入http://localhost:1111,可以看到erueka注册中心页面创建服务提供者<?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>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>say-hello</artifactId> <version>0.0.1-SNAPSHOT</version> <name>say-hello</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>配置文件:server.port=2222#服务名称,服务与服务之间相互调用一般都是根据这个namespring.application.name=say-hello#注册中心地址eureka.client.service-url.defaultZone=http://localhost:1111/eureka/创建一个Controller@RestControllerpublic class SayHelloController { @Value("${server.port}”) private String serverPort; @GetMapping("/sayHello”) public String sayHelloCtr(@RequestParam(“helloName”)String helloName){ return “Hello “+helloName+",我的端口是: “+serverPort; }}在启动类上添加@EnableEurekaClient表明自己是一个eureka client@SpringBootApplication@EnableEurekaClientpublic class SayHelloApplication { public static void main(String[] args) { SpringApplication.run(SayHelloApplication.class, args); }}启动服务访问http://localhost:1111,会发现服务已经注册到注册中心中去了。Eureka自我保护机制什么是自我保护 自我保护模式是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的 健壮、稳定的运行。工作机制 Eureka Server 如果在eureka注册中心中看见“EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE“这样一段话,则说明erueka进入自我保护。自我保护模式被激活的条件是:在 1 分钟后,Renews (last min)(Eureka Server 最后 1 分钟收到客户端实例续约的总数。) < Renews threshold(Eureka Server 期望每分钟收到客户端实例续约的总数。)。eureka在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期。一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,在开发是可以配置eureka.server.enable-self-preservation=false关闭自我保护,这样可以确保注册中心中不可用的实例被及时的剔除eureka不清除已关节点问题 在开发过程中,我们常常希望Eureka Server能够迅速有效地踢出已关停的节点,但是由于Eureka自我保护模式,以及心跳周期长的原因,常常会遇到Eureka Server不踢出已关停的节点的问题 解决办法: Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔。 eureka.server.enable-self-preservation # 设为false,关闭自我保护 eureka.server.eviction-interval-timer-in-ms # 清理间隔(单位毫秒,默认是60*1000) Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间。 eureka.client.healthcheck.enabled # 开启健康检查(需要spring-boot-starter-actuator依赖) eureka.instance.lease-renewal-interval-in-seconds # 续约更新时间间隔(默认30秒) eureka.instance.lease-expiration-duration-in-seconds # 续约到期时间(默认90秒) 注:更改Eureka更新频率将打破服务器的自我保护功能,生产环境下不建议自定义这些配置。eureka服务端常用配置 (1)enable-self-preservation: true # 自我保护模式,当出现出现网络分区、eureka在短时间内丢失过多客户端时,会进入自我保护模式,即一个服务长时间没有发送心跳,eureka也不会将其删除,默认为true (2)eviction-interval-timer-in-ms: 60000 #eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒 (3)renewal-percent-threshold: 0.85 #阈值因子,默认是0.85,如果阈值比最小值大,则自我保护模式开启 (4)peer-eureka-nodes-update-interval-ms: 600000 # 集群里eureka节点的变化信息更新的时间间隔,单位为毫秒,默认为10 * 60 * 1000eureka客户端常用配置 (1)register-with-eureka: false #是否注册到eureka (2)registry-fetch-interval-seconds: 30 # 从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒 (3)fetch-registry: false # 实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true 如果是做高可用的发现服务那就要改成true (4)instance-info-replication-interval-seconds: 30 # 复制实例变化信息到eureka服务器所需要的时间间隔(s),默认为30秒 (5)initial-instance-info-replication-interval-seconds: 40 # 最初复制实例信息到eureka服务器所需的时间(s),默认为40秒 (6)eureka-service-url-poll-interval-seconds: 300 #询问Eureka服务url信息变化的时间间隔(s),默认为300秒 (7)eureka-server-connect-timeout-seconds: 5 # eureka需要超时连接之前需要等待的时间,默认为5秒 (8)eureka-server-total-connections: 200 #eureka客户端允许所有eureka服务器连接的总数目,默认是200 (9)heartbeat-executor-thread-pool-size: 2 #心跳执行程序线程池的大小,默认为2 (10)cache-refresh-executor-thread-pool-size: 2 # 执行程序缓存刷新线程池的大小,默认为2 ...

January 10, 2019 · 2 min · jiezi

@PropertySource 分环境读取配置

工作的时候,一般来说代码都是分环境的,比如dev,test,prd什么的,在用到@PropertySource 注解的时候,发现好像不能根据环境读取自定义的.properties文件,比如我有个systemProperties-dev.properties文件,一开始只是systemProperties-${spring.profiles.active}.properties这样的方式勉强能用,但是后来当我的环境变量变成多环境的时候,也就是spring.profiles.active = dev,test这样的是,这个方法就不奏效了,(多傻啊,其实早就想到了,他会直接在“-”后面拼了一个“dev,test”)然后在网上看了看资料,参考了以下的一篇文章,然后参照了下源码,用了一个比较简单,但是很难看的方法实现了:P(感觉也是暂时解决问题。)。参照文章:Springboot中PropertySource注解多环境支持以及原理主要思想,重写PropertySourceFactory,在PropertySourceFactory中,重新取得resource,SystemProperties.java@Component@PropertySource(name=“systemConfig”, value = {“classpath:/systemConfig-${spring.profiles.active}.properties”}, factory = SystemPropertySourceFactory.class)public class SystemProperties { // 自己的内容…. }这里指定了 factory = SystemPropertySourceFactory.class,接下来SystemPropertySourceFactory.java@Configurationpublic class SystemPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException { FileSystemResourceLoader resourceLoader = new FileSystemResourceLoader(); //取得当前活动的环境名称(因为直接获取spring.profiles.active 失败,所以才把环境名称拼在文件名后面来拿) //其实感觉应该有可以直接取的方法比如从环境里取 String[] actives = encodedResource.getResource().getFilename().split("\.")[0].replace(name + “-”, “”).split(","); //如果只有一个,就直接返回 if (actives.length <= 1) { return (name != null ? new ResourcePropertySource(name, encodedResource) : new ResourcePropertySource(encodedResource)); } //如果是多个 List<URL> resourceUrls = new ArrayList<>(); //遍历后把所有环境的url全部抓取到list中 Arrays.stream(actives).forEach(active -> { //在resource目录下读取配置文件 URL url = this.getClass().getResource("/" + name.concat("-" + active).concat(".properties")); if (url != null) { resourceUrls.add(url); } }); if (resourceUrls != null && resourceUrls.size() > 0) { List<InputStream> inputStreamList = new ArrayList<>(); //取得所有资源的inputStream for (URL url : resourceUrls) { Resource resource0 = resourceLoader.getResource(url.getPath()); InputStream in = resource0.getInputStream(); inputStreamList.add(in); } //串行流,将多个文件流合并车一个流 SequenceInputStream inputStream = new SequenceInputStream(Collections.enumeration(inputStreamList)); //转成resource InputStreamResource resource = new InputStreamResource(inputStream); return (name != null ? new ResourcePropertySource(name, new EncodedResource(resource)) : new ResourcePropertySource(new EncodedResource(resource))); } else { return (name != null ? new ResourcePropertySource(name, encodedResource) : new ResourcePropertySource(encodedResource)); } }}这样实现后,就能将多个环境的Property文件加载进去了。然后是关于spring.profiles.active 为什么要这么取,我试过@value,和用Environment 对象,都取不到,可能跟bean创建的先后顺序有关。没有继续调查,希望知道原因的朋友能帮忙解答~ ...

January 9, 2019 · 1 min · jiezi

跨域携带cookies无效问题

开发环境:vue,axios 0.17.1,springboot 2.1.1,springsession在本地测试页面时,发现cookies都没有传上去,本地测试是跨域的,原先是正常的。开始以为是axios问题,结果试了XMLHttpRequest也是一样,都已设置withCredentials:true。跨域的请求都能接收和回应,但是请求时cookies都没有携带。看了下set-cookie的值:SESSION=YWFlZTBjY2QtOWE4NC00MmI4LWEwZWEtYjUxYzY2ZjMyN2Nh; Path=/server/; HttpOnly; SameSite=Lax,这里多了个HttpOnly和SameSite,而问题就出在这个SameSite上。(可能是更新到SpringSession2之后导致的)取消SameSite:// SpringSession配置类@EnableRedisHttpSession( maxInactiveIntervalInSeconds = 7200)public class SpringSessionConfig { public SpringSessionConfig() {} @Bean public CookieSerializer httpSessionIdResolver() { DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); // 取消samesite cookieSerializer.setSameSite(null); return cookieSerializer; }}ps:chrome的network中看不到跨域的set-cookie标记,也是坑。

January 9, 2019 · 1 min · jiezi

Spring跨域配置

Spring跨域配置介绍跨站 HTTP 请求(Cross-site HTTP request)是指发起请求的资源所在域不同于该请求所指向资源所在的域的 HTTP 请求。比如说,域名A(http://domaina.example)的某 Web 应用程序中通过标签引入了域名B(http://domainb.foo)站点的某图片资源(http://domainb.foo/image.jpg),域名A的那 Web 应用就会导致浏览器发起一个跨站 HTTP 请求。在当今的 Web 开发中,使用跨站 HTTP 请求加载各类资源(包括CSS、图片、JavaScript 脚本以及其它类资源),已经成为了一种普遍且流行的方式。 出于安全考虑,浏览器会限制脚本中发起的跨站请求。比如,使用 XMLHttpRequest 对象发起 HTTP 请求就必须遵守同源策略(same-origin policy)。 具体而言,Web 应用程序能且只能使用 XMLHttpRequest 对象向其加载的源域名发起 HTTP 请求,而不能向任何其它域名发起请求。为了能开发出更强大、更丰富、更安全的Web应用程序,开发人员渴望着在不丢失安全的前提下,Web 应用技术能越来越强大、越来越丰富。比如,可以使用 XMLHttpRequest 发起跨站 HTTP 请求。(这段描述跨域不准确,跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是crsf跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。)普通参数跨域在response的头文件添加httpServletResponse.setHeader(“Access-Control-Allow-Origin”,"");httpServletResponse.setHeader(“Access-Control-Allow-Methods”,“POST”);httpServletResponse.setHeader(“Access-Control-Allow-Headers”,“Access-Control”);httpServletResponse.setHeader(“Allow”,“POST”);参数值描述Access-Control-Allow-Origin授权的源控制Access-Control-Allow-Credentialstrue / false是否允许用户发送和处理cookieAccess-Control-Allow-Methods[,]*允许请求的HTTP Method,多个用逗号分隔Access-Control-Allow-Headers[,]控制哪些header能发送真正的请求,多个用逗号分隔Access-Control-Max-Age秒授权的时间,单位为秒。有效期内,不会重复发送预检请求带headr请求跨域这样客户端需要发起OPTIONS请求, 可以说是一个【预请求】,用于探测后续真正需要发起的跨域 POST 请求对于服务器来说是否是安全可接受的,因为跨域提交数据对于服务器来说可能存在很大的安全问题。因为Springmvc模式是关闭OPTIONS请求的,所以需要开启<servlet> <servlet-name>application</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>dispatchOptionsRequest</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup></servlet>开启CORSSpringMVC从4.2版本开始增加了对CORS的支持。在springMVC 中增加CORS支持非常简单,可以配置全局的规则,也可以使用@CrossOrigin注解进行细粒度的配置。使用@CrossOrigin注解先通过源码看看该注解支持的属性在Controller上使用@CrossOrigin注解// 指定当前的AccountController中所有的方法可以处理所有域上的请求@CrossOrigin(origins = {“http://domain1.com”, “http://domain2.com”}, maxAge = 72000L)@RestController@RequestMapping("/account")public class AccountController { @RequestMapping("/{id}") public Account retrieve(@PathVariable Long id) { // … } @RequestMapping(method = RequestMethod.DELETE, path = “/{id}”) public void remove(@PathVariable Long id) { // … }}在方法上使用@CrossOrigin注解@CrossOrigin(maxAge = 3600L)@RestController@RequestMapping("/account")public class AccountController { @CrossOrigin(origins = {“http://domain1.com”, “http://domain2.com”}) @RequestMapping("/{id}") public Account retrieve(@PathVariable Long id) { // … } @RequestMapping(method = RequestMethod.DELETE, path = “/{id}”) public void remove(@PathVariable Long id) { // … }}CORS全局配置除了细粒度基于注解的配置,可以定义全局CORS的配置。这类似于使用过滤器,但可以在Spring MVC中声明,并结合细粒度@CrossOrigin配置。默认情况下所有的域名和GET、HEAD和POST方法都是允许的。基于XML的配置<mvc:cors> <mvc:mapping path="/api/" allowed-origins=“http://domain1.com, http://domain2.com” allowed-methods=“GET, POST, PUT, HEAD, PATCH, DELETE, OPTIONS, TRACE” allowed-headers=“header1, header2, header3” exposed-headers=“header1, header2” allow-credentials=“false” max-age=“72000” /> <mvc:mapping path="/resources/" allowed-origins=“http://domain3.com” /></mvc:cors>基于代码的配置这个方法同样适用于SpringBoot。@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/") .allowedOrigins(“http://domain1.com”) .allowedOrigins(“http://domain2.com”) .allowedMethods(“GET”, “POST”, “PUT”, “HEAD”, “PATCH”, “DELETE”, “OPTIONS”, “TRACE”); .allowedHeaders(“header1”, “header2”, “header3”) .exposedHeaders(“header1”, “header2”) .allowCredentials(false) .maxAge(72000L); registry.addMapping("/resources/") .allowedOrigins(“http://domain3.com”); }}基于过滤器的配置import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;@Beanpublic FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin(“http://domain1.com”); config.addAllowedOrigin(“http://domain2.com”); config.addAllowedHeader(""); config.addAllowedMethod(“GET, POST, PUT, HEAD, PATCH, DELETE, OPTIONS, TRACE”); config.setAllowCredentials(true); config.setMaxAge(72000L); // CORS 配置对所有接口都有效 source.registerCorsConfiguration("/**", config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); bean.setOrder(0); return bean;} ...

January 9, 2019 · 2 min · jiezi

Spring Boot Admin 2.1.0 全攻略

转载请标明出处: https://www.fangzhipeng.com本文出自方志朋的博客Spring Boot Admin简介Spring Boot Admin是一个开源社区项目,用于管理和监控SpringBoot应用程序。 应用程序作为Spring Boot Admin Client向为Spring Boot Admin Server注册(通过HTTP)或使用SpringCloud注册中心(例如Eureka,Consul)发现。 UI是的AngularJs应用程序,展示Spring Boot Admin Client的Actuator端点上的一些监控。常见的功能或者监控如下:显示健康状况显示详细信息,例如JVM和内存指标micrometer.io指标数据源指标缓存指标显示构建信息编号关注并下载日志文件查看jvm系统和环境属性查看Spring Boot配置属性支持Spring Cloud的postable / env-和/ refresh-endpoint轻松的日志级管理与JMX-beans交互查看线程转储查看http跟踪查看auditevents查看http-endpoints查看计划任务查看和删除活动会话(使用spring-session)查看Flyway / Liquibase数据库迁移下载heapdump状态变更通知(通过电子邮件,Slack,Hipchat,……)状态更改的事件日志(非持久性)快速开始创建Spring Boot Admin Server本文的所有工程的Spring Boot版本为2.1.0 、Spring Cloud版本为Finchley.SR2。案例采用Maven多module形式,父pom文件引入以下的依赖(完整的依赖见源码): <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.0.RELEASE</version> <relativePath/> </parent> <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> <spring-cloud.version>Finchley.SR2</spring-cloud.version>在工程admin-server引入admin-server的起来依赖和web的起步依赖,代码如下:<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.1.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>然后在工程的启动类AdminServerApplication加上@EnableAdminServer注解,开启AdminServer的功能,代码如下:@SpringBootApplication@EnableAdminServerpublic class AdminServerApplication { public static void main(String[] args) { SpringApplication.run( AdminServerApplication.class, args ); }}在工程的配置文件application.yml中配置程序名和程序的端口,代码如下:spring: application: name: admin-serverserver: port: 8769这样Admin Server就创建好了。创建Spring Boot Admin Client在admin-client工程的pom文件引入admin-client的起步依赖和web的起步依赖,代码如下: <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>在工程的配置文件application.yml中配置应用名和端口信息,以及向admin-server注册的地址为http://localhost:8769,最后暴露自己的actuator的所有端口信息,具体配置如下:spring: application: name: admin-client boot: admin: client: url: http://localhost:8769server: port: 8768management: endpoints: web: exposure: include: ‘’ endpoint: health: show-details: ALWAYS在工程的启动文件如下:@SpringBootApplicationpublic class AdminClientApplication { public static void main(String[] args) { SpringApplication.run( AdminClientApplication.class, args ); }一次启动两个工程,在浏览器上输入localhost:8769 ,浏览器显示的界面如下:查看wallboard:点击wallboard,可以查看admin-client具体的信息,比如内存状态信息:也可以查看spring bean的情况:更多监控信息,自己体验。Spring boot Admin结合SC注册中心使用同上一个案例一样,本案例也是使用的是Spring Boot版本为2.1.0 、Spring Cloud版本为Finchley.SR2。案例采用Maven多module形式,父pom文件引入以下的依赖(完整的依赖见源码),此处省略。搭建注册中心注册中心使用Eureka、使用Consul也是可以的,在eureka-server工程中的pom文件中引入: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>配置eureka-server的端口信息,以及defaultZone和防止自注册。最后系统暴露eureka-server的actuator的所有端口。spring: application: name: eureka-serverserver: port: 8761eureka: client: service-url: defaultZone: http://localhost:8761/eureka register-with-eureka: false fetch-registry: falsemanagement: endpoints: web: exposure: include: “” endpoint: health: show-details: ALWAYS在工程的启动文件EurekaServerApplication加上@EnableEurekaServer注解开启Eureka Server.@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run( EurekaServerApplication.class, args ); }}eureka-server搭建完毕。搭建admin-server在admin-server工程的pom文件引入admin-server的起步依赖、web的起步依赖、eureka-client的起步依赖,如下:<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.1.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>然后配置admin-server,应用名、端口信息。并向注册中心注册,注册地址为http://localhost:8761,最后将actuator的所有端口暴露出来,配置如下:spring: application: name: admin-serverserver: port: 8769eureka: client: registryFetchIntervalSeconds: 5 service-url: defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/ instance: leaseRenewalIntervalInSeconds: 10 health-check-url-path: /actuator/healthmanagement: endpoints: web: exposure: include: “” endpoint: health: show-details: ALWAYS在工程的启动类AdminServerApplication加上@EnableAdminServer注解,开启admin server的功能,加上@EnableDiscoveryClient注解开启eurke client的功能。@SpringBootApplication@EnableAdminServer@EnableDiscoveryClientpublic class AdminServerApplication { public static void main(String[] args) { SpringApplication.run( AdminServerApplication.class, args ); }}搭建admin-client在admin-client的pom文件引入以下的依赖,由于2.1.0采用webflux,引入webflux的起步依赖,引入eureka-client的起步依赖,并引用actuator的起步依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>在工程的配置文件配置应用名、端口、向注册中心注册的地址,以及暴露actuator的所有端口。spring: application: name: admin-clienteureka: instance: leaseRenewalIntervalInSeconds: 10 health-check-url-path: /actuator/health client: registryFetchIntervalSeconds: 5 service-url: defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/management: endpoints: web: exposure: include: “” endpoint: health: show-details: ALWAYSserver: port: 8762在启动类加上@EnableDiscoveryClie注解,开启DiscoveryClient的功能。@SpringBootApplication@EnableDiscoveryClientpublic class AdminClientApplication { public static void main(String[] args) { SpringApplication.run( AdminClientApplication.class, args ); }}一次启动三个工程,在浏览器上访问localhost:8769,浏览器会显示和上一小节一样的界面。集成spring security在2.1.0版本中去掉了hystrix dashboard,登录界面默认集成到了spring security模块,只要加上spring security就集成了登录模块。只需要改变下admin-server工程,需要在admin-server工程的pom文件引入以下的依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>在admin-server工的配置文件application.yml中配置spring security的用户名和密码,这时需要在服务注册时带上metadata-map的信息,如下:spring: security: user: name: “admin” password: “admin” eureka: instance: metadata-map: user.name: ${spring.security.user.name} user.password: ${spring.security.user.password}写一个配置类SecuritySecureConfig继承WebSecurityConfigurerAdapter,配置如下:@Configurationpublic class SecuritySecureConfig extends WebSecurityConfigurerAdapter { private final String adminContextPath; public SecuritySecureConfig(AdminServerProperties adminServerProperties) { this.adminContextPath = adminServerProperties.getContextPath(); } @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setTargetUrlParameter( “redirectTo” ); http.authorizeRequests() .antMatchers( adminContextPath + “/assets/**” ).permitAll() .antMatchers( adminContextPath + “/login” ).permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage( adminContextPath + “/login” ).successHandler( successHandler ).and() .logout().logoutUrl( adminContextPath + “/logout” ).and() .httpBasic().and() .csrf().disable(); // @formatter:on }}重启启动工程,在浏览器上访问:http://localhost:8769/,会被重定向到登录界面,登录的用户名和密码为配置文件中配置的,分别为admin和admin,界面显示如下:集成邮箱报警功能在spring boot admin中,也可以集成邮箱报警功能,比如服务不健康了、下线了,都可以给指定邮箱发送邮件。集成非常简单,只需要改造下admin-server即可:在admin-server工程Pom文件,加上mail的起步依赖,代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId></dependency>在配置文件application.yml文件中,需要配置邮件相关的配置,如下:spring.mail.host: smtp.163.comspring.mail.username: miles02spring.mail.password:spring.boot.admin.notify.mail.to: 124746406@qq.com做完以上配置后,当我们已注册的客户端的状态从 UP 变为 OFFLINE 或其他状态,服务端就会自动将电子邮件发送到上面配置的地址。源码下载快速开始: https://github.com/forezp/Spr…和spring cloud结合:https://github.com/forezp/Spr…参考资料http://codecentric.github.io/…https://github.com/codecentri…更多阅读史上最简单的 SpringCloud 教程汇总SpringBoot教程汇总Java面试题系列汇总<div><p align=“center”> <img src=“https://www.fangzhipeng.com/img/avatar.jpg" width=“258” height=“258”/> <br> 扫一扫,支持下作者吧</p><p align=“center” style=“margin-top: 15px; font-size: 11px;color: #cc0000;"> <strong>(转载本站文章请注明作者和出处 <a href=“https://www.fangzhipeng.com”>方志朋的博客</a>)</strong></p></div> ...

January 9, 2019 · 2 min · jiezi

SpringBoot集成MQTT

SpringBoot集成MQTTMQTTMQTT(消息队列遥测传输)是ISO标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议。国内很多企业都广泛使用MQTT作为Android手机客户端与服务器端推送消息的协议。特点MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,它具有以下主要的几项特性:使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合;对负载内容屏蔽的消息传输;使用TCP/IP提供网络连接;有三种消息发布服务质量;至多一次:消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。至少一次:确保消息到达,但消息重复可能会发生。只有一次:确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量;使用Last Will和Testament特性通知有关各方客户端异常中断的机制。Apache-ApolloApache Apollo是一个代理服务器,其是在ActiveMQ基础上发展而来的,可以支持STOMP, AMQP, MQTT, Openwire, SSL, WebSockets 等多种协议。 原理:服务器端创建一个唯一订阅号,发送者可以向这个订阅号中发东西,然后接受者(即订阅了这个订阅号的人)都会收到这个订阅号发出来的消息。以此来完成消息的推送。服务器其实是一个消息中转站。下载下载地址:http://activemq.apache.org/ap…配置与启动需要安装JDK环境在命令行模式下进入bin,执行apollo create mybroker d:\apache-apollo\broker,创建一个名为mybroker虚拟主机(Virtual Host)。需要特别注意的是,生成的目录就是以后真正启动程序的位置。在命令行模式下进入d:\apache-apollo\broker\bin,执行apollo-broker run,也可以用apollo-broker-service.exe配置服务。访问http://127.0.0.1:61680打开web管理界面。(密码查看broker/etc/users.properties)启动端口,看cmd输出。SpringBoot2的开发添加依赖<!– spring-boot版本 2.1.0.RELEASE–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId></dependency><dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-stream</artifactId></dependency><dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId></dependency>自定义配置# src/main/resources/config/mqtt.properties################### MQTT 配置################### 用户名mqtt.username=admin# 密码mqtt.password=password# 推送信息的连接地址,如果有多个,用逗号隔开,如:tcp://127.0.0.1:61613,tcp://192.168.1.61:61613mqtt.url=tcp://127.0.0.1:61613################### MQTT 生产者################### 连接服务器默认客户端IDmqtt.producer.clientId=mqttProducer# 默认的推送主题,实际可在调用接口时指定mqtt.producer.defaultTopic=topic1################### MQTT 消费者################### 连接服务器默认客户端IDmqtt.consumer.clientId=mqttConsumer# 默认的接收主题,可以订阅多个Topic,逗号分隔mqtt.consumer.defaultTopic=topic1配置MQTT发布和订阅import org.apache.commons.lang3.StringUtils;import org.eclipse.paho.client.mqttv3.MqttConnectOptions;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.integration.annotation.ServiceActivator;import org.springframework.integration.channel.DirectChannel;import org.springframework.integration.core.MessageProducer;import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;import org.springframework.integration.mqtt.core.MqttPahoClientFactory;import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;import org.springframework.messaging.Message;import org.springframework.messaging.MessageChannel;import org.springframework.messaging.MessageHandler;import org.springframework.messaging.MessagingException;/** * MQTT配置,生产者 * * @author BBF /@Configurationpublic class MqttConfig { private static final Logger LOGGER = LoggerFactory.getLogger(MqttConfig.class); private static final byte[] WILL_DATA; static { WILL_DATA = “offline”.getBytes(); } /* * 订阅的bean名称 / public static final String CHANNEL_NAME_IN = “mqttInboundChannel”; /* * 发布的bean名称 / public static final String CHANNEL_NAME_OUT = “mqttOutboundChannel”; @Value("${mqtt.username}") private String username; @Value("${mqtt.password}") private String password; @Value("${mqtt.url}") private String url; @Value("${mqtt.producer.clientId}") private String producerClientId; @Value("${mqtt.producer.defaultTopic}") private String producerDefaultTopic; @Value("${mqtt.consumer.clientId}") private String consumerClientId; @Value("${mqtt.consumer.defaultTopic}") private String consumerDefaultTopic; /* * MQTT连接器选项 * * @return {@link org.eclipse.paho.client.mqttv3.MqttConnectOptions} / @Bean public MqttConnectOptions getMqttConnectOptions() { MqttConnectOptions options = new MqttConnectOptions(); // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录, // 这里设置为true表示每次连接到服务器都以新的身份连接 options.setCleanSession(true); // 设置连接的用户名 options.setUserName(username); // 设置连接的密码 options.setPassword(password.toCharArray()); options.setServerURIs(StringUtils.split(url, “,”)); // 设置超时时间 单位为秒 options.setConnectionTimeout(10); // 设置会话心跳时间 单位为秒 服务器会每隔1.520秒的时间向客户端发送心跳判断客户端是否在线,但这个方法并没有重连的机制 options.setKeepAliveInterval(20); // 设置“遗嘱”消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的“遗嘱”消息。 options.setWill(“willTopic”, WILL_DATA, 2, false); return options; } /** * MQTT客户端 * * @return {@link org.springframework.integration.mqtt.core.MqttPahoClientFactory} / @Bean public MqttPahoClientFactory mqttClientFactory() { DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); factory.setConnectionOptions(getMqttConnectOptions()); return factory; } /* * MQTT信息通道(生产者) * * @return {@link org.springframework.messaging.MessageChannel} / @Bean(name = CHANNEL_NAME_OUT) public MessageChannel mqttOutboundChannel() { return new DirectChannel(); } /* * MQTT消息处理器(生产者) * * @return {@link org.springframework.messaging.MessageHandler} / @Bean @ServiceActivator(inputChannel = CHANNEL_NAME_OUT) public MessageHandler mqttOutbound() { MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler( producerClientId, mqttClientFactory()); messageHandler.setAsync(true); messageHandler.setDefaultTopic(producerDefaultTopic); return messageHandler; } /* * MQTT消息订阅绑定(消费者) * * @return {@link org.springframework.integration.core.MessageProducer} / @Bean public MessageProducer inbound() { // 可以同时消费(订阅)多个Topic MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter( consumerClientId, mqttClientFactory(), StringUtils.split(consumerDefaultTopic, “,”)); adapter.setCompletionTimeout(5000); adapter.setConverter(new DefaultPahoMessageConverter()); adapter.setQos(1); // 设置订阅通道 adapter.setOutputChannel(mqttInboundChannel()); return adapter; } /* * MQTT信息通道(消费者) * * @return {@link org.springframework.messaging.MessageChannel} / @Bean(name = CHANNEL_NAME_IN) public MessageChannel mqttInboundChannel() { return new DirectChannel(); } /* * MQTT消息处理器(消费者) * * @return {@link org.springframework.messaging.MessageHandler} / @Bean @ServiceActivator(inputChannel = CHANNEL_NAME_IN) public MessageHandler handler() { return new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { LOGGER.error("===================={}============", message.getPayload()); } }; }}消息发布器import org.springframework.integration.annotation.MessagingGateway;import org.springframework.integration.mqtt.support.MqttHeaders;import org.springframework.messaging.handler.annotation.Header;import org.springframework.stereotype.Component;/* * MQTT生产者消息发送接口 * <p>MessagingGateway要指定生产者的通道名称</p> * @author BBF /@Component@MessagingGateway(defaultRequestChannel = MqttConfig.CHANNEL_NAME_OUT)public interface IMqttSender { /* * 发送信息到MQTT服务器 * * @param data 发送的文本 / void sendToMqtt(String data); /* * 发送信息到MQTT服务器 * * @param topic 主题 * @param payload 消息主体 / void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload); /* * 发送信息到MQTT服务器 * * @param topic 主题 * @param qos 对消息处理的几种机制。<br> 0 表示的是订阅者没收到消息不会再次发送,消息会丢失。<br> * 1 表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息。<br> * 2 多了一次去重的动作,确保订阅者收到的消息有一次。 * @param payload 消息主体 / void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);}发送消息/* * MQTT消息发送 * * @author BBF /@Controller@RequestMapping(value = “/")public class MqttController { /* * 注入发送MQTT的Bean / @Resource private IMqttSender iMqttSender; /* * 发送MQTT消息 * @param message 消息内容 * @return 返回 / @ResponseBody @GetMapping(value = “/mqtt”, produces =“text/html”) public ResponseEntity<String> sendMqtt(@RequestParam(value = “msg”) String message) { iMqttSender.sendToMqtt(message); return new ResponseEntity<>(“OK”, HttpStatus.OK); }}入口类import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;import org.springframework.context.annotation.PropertySource;/* * SpringBoot 入口类 * * @author BBF */@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})@PropertySource(encoding = “UTF-8”, value = {“classpath:config/mqtt.properties”})public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }} ...

January 8, 2019 · 3 min · jiezi

微服务实战 - docker-compose实现mysql+springboot+angular

前言这是一次完整的项目实践,Angular页面+Springboot接口+MySQL都通过Dockerfile打包成docker镜像,通过docker-compose做统一编排。目的是实现整个项目产品的轻量级和灵活性,在将各个模块的镜像都上传公共镜像仓库后,任何人都可以通过 “docker-compose up -d” 一行命令,将整个项目的前端、后端、数据库以及文件服务器等,运行在自己的服务器上。本项目是开发一个类似于segmentfault的文章共享社区,我的设想是当部署在个人服务器上时就是个人的文章库,部署在项目组的服务器上就是项目内部的文章库,部署在公司的服务器上就是所有职工的文章共享社区。突出的特点就是,项目相关的所有应用和文件资源都是灵活的,用户可以傻瓜式地部署并使用,对宿主机没有任何依赖。目前一共有三个docker镜像,考虑以后打在一个镜像中,但目前只能通过docker-compose来编排这三个镜像。MySQL镜像:以MySQL为基础,将项目所用到的数据库、表结构以及一些基础表的数据库,通过SQL脚本打包在镜像中。用户在启动镜像后就自动创建了项目所有的数据库、表和基础表数据。SpringBoot镜像:后台接口通过SpringBoot开发,开发完成后直接可以打成镜像,由于其内置tomcat,可以直接运行,数据库指向启动好的MySQL容器中的数据库。Nginx(Angular)镜像:Nginx镜像中打包了Angular项目的dist目录资源,以及default.conf文件。主要的作用有:部署Angular项目页面;挂载宿主机目录作为文件服务器;以及反向代理SpringBoot接口,解决跨域问题等等。最后三个docker容器的编排通过docker-compose来实现,三个容器之间的相互访问都通过容器内部的别名,避免了宿主机迁移时ip无法对应的问题。为了方便开发,顺便配了个自动部署。MySQL镜像初始化脚本在项目完成后,需要生成项目所需数据库、表结构以及基础表数据的脚本,保证在运行该docker容器中,启动MySQL数据库时,自动构建数据库和表结构,并初始化基础表数据。Navicat for MySQL的客户端支持导出数据库的表结构和表数据的SQL脚本。如果没有安装Navicat,可以在连接上容器中开发用的MySQL数据库,通过mysqldump 命令导出数据库表结构和数据的SQL脚本。下文中就是将数据库的SQL脚本导出到宿主机的/bees/sql 目录:docker exec -it mysql mysqldump -uroot -pPASSWORD 数据库名称 > /bees/sql/数据库名称.sql以上只是导出 表结构和表数据的脚本,还要在SQL脚本最上方加上 生成数据库的SQL:drop database if exists 数据库名称;create database 数据库名称;use 数据库名称;通过以上两个步骤,数据库、表结构和表数据三者的初始化SQL脚本就生成好了。Dockerfile构建镜像我们生成的SQL脚本叫 bees.sql,在MySQL官方镜像中提供了容器启动时自动执行/docker-entrypoint-initdb.d文件夹下的脚本的功能(包括shell脚本和sql脚本),我们在后续生成镜像的时候,将上述生成的SQL脚本COPY到MySQL的/docker-entrypoint-initdb.d目录下就可以了。现在我们写Dockerfile,很简单:FROM mysqlMAINTAINER kerry(kerry.wu@definesys.com)COPY bees.sql /docker-entrypoint-initdb.d将 bees.sql 和 Dockerfile 两个文件放在同一目录,执行构建镜像的命令就可以了:docker build -t bees-mysql .现在通过 docker images,就能看到本地的镜像库中就已经新建了一个 bees-mysql的镜像啦。SpringBoot镜像springboot构建镜像的方式很多,有通过代码生成镜像的,也有通过jar包生成镜像的。我不想对代码有任何污染,就选择后者,通过生成的jar包构建镜像。创建一个目录,上传已经准备好的springboot的jar包,这里名为bees-0.0.1-SNAPSHOT.jar,然后同样编写Dockerfile文件:FROM java:8VOLUME /tmpADD bees-0.0.1-SNAPSHOT.jar /bees-springboot.jarEXPOSE 8010ENTRYPOINT [“java”,"-Djava.security.egd=file:/dev/./urandom","-jar","-Denv=DEV","/bees-springboot.jar"]将bees-0.0.1-SNAPSHOT.jar和Dockerfile放在同一目录执行命令开始构建镜像,同样在本地镜像库中就生成了bees-springboot的镜像:docker build -t bees-springboot .Nginx(Angular)镜像Nginx的配置该镜像主要在于nginx上conf.d/default.conf文件的配置,主要实现三个需求:1、Angualr部署Angular的部署很简单,只要将Angular项目通过 ng build –prod 命令生成dist目录,将dist目录作为静态资源文件放在服务器上访问就行。我们这里就把dist目录打包在nginx容器中,在default.conf上配置访问。2、文件服务器项目为文章共享社区,少不了的就是一个存储文章的文件服务器,包括存储一些图片之类的静态资源。需要在容器中创建一个文件目录,通过default.conf上的配置将该目录代理出来,可以直接访问目录中的文件。当然为了不丢失,这些文件最好是保存在宿主机上,在启动容器时可以将宿主机本地的目录挂载到容器中的文件目录。3、接口跨域问题在前后端分离开发的项目中,“跨域问题”是较为常见的,SpringBoot的容器和Angular所在的容器不在同一个ip和端口,我们同样可以在default.conf上配置反向代理,将后台接口代理成同一个ip和端口的地址。话不多说,结合上面三个问题,我们最终的default.conf为:server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://beesSpringboot:8010/; } location /file { alias /home/file; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; }}location / :代理的是Angular项目,dist目录内通过Dockerfile COPY在容器内的/usr/share/nginx/html目录;location /file :代理/home/file 目录,作为文件服务器;location /api/ :是为了解决跨域而做的反向代理,为了脱离宿主机的限制,接口所在容器的ip通过别名beesSpringboot来代替。别名的设置是在docker-compose.yml中设置的,后续再讲。Dockerfile构建镜像同样创建一个目录,包含Angualr的dist目录、Dockerfile和nginx的default.conf文件,目录结构如下:[root@Kerry angular]# tree.├── dist│ └── Bees│ ├── 0.cb202cb30edaa3c93602.js│ ├── 1.3ac3c111a5945a7fdac6.js│ ├── 2.99bfc194c4daea8390b3.js│ ├── 3.50547336e0234937eb51.js│ ├── 3rdpartylicenses.txt│ ├── 4.53141e3db614f9aa6fe0.js│ ├── assets│ │ └── images│ │ ├── login_background.jpg│ │ └── logo.png│ ├── favicon.ico│ ├── index.html│ ├── login_background.7eaf4f9ce82855adb045.jpg│ ├── main.894e80999bf907c5627b.js│ ├── polyfills.6960d5ea49e64403a1af.js│ ├── runtime.37fed2633286b6e47576.js│ └── styles.9e4729a9c6b60618a6c6.css├── Dockerfile└── nginx └── default.confDockerfile文件如下:FROM nginxCOPY nginx/default.conf /etc/nginx/conf.d/RUN rm -rf /usr/share/nginx/html/*COPY /dist/Bees /usr/share/nginx/htmlCMD [“nginx”, “-g”, “daemon off;"]以上,通过下列命令,构建bees-nginx-angular的镜像完成:docker build -t bees-nginx-angular . docker-compose容器服务编排上述,我们已经构建了三个镜像,相对应的至少要启动三个容器来完成项目的运行。那要执行三个docker run?太麻烦了,而且这三个容器之间还需要相互通信,如果只使用docker来做的话,不光启动容器的命令会很长,而且为了容器之间的通信,docker –link 都会十分复杂,这里我们需要一个服务编排。docker的编排名气最大的当然是kubernetes,但我的初衷是让这个项目轻量级,不太希望用户安装偏重量级的kubernetes才能运行,而我暂时又没能解决将三个镜像构建成一个镜像的技术问题,就选择了适中的一个产品–docker-compse。安装docker-compose很简单,这里就不赘言了。安装完之后,随便找个目录,写一个docker-compose.yml文件,然后在该文件所在地方执行一行命令就能将三个容器启动了:#启动docker-compose up -d#关闭docker-compose down这里直接上我写的docker-compose.yml文件version: “2"services: beesMysql: restart: always image: bees-mysql ports: - 3306:3306 volumes: - /bees/docker_volume/mysql/conf:/etc/mysql/conf.d - /bees/docker_volume/mysql/logs:/logs - /bees/docker_volume/mysql/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: kerry beesSpringboot: restart: always image: bees-springboot ports: - 8010:8010 depends_on: - beesMysql beesNginxAngular: restart: always image: bees-nginx-angular ports: - 8000:80 depends_on: - beesSpringboot volumes: - /bees/docker_volume/nginx/nginx.conf:/etc/nginx/nginx.conf - /bees/docker_volume/nginx/conf.d:/etc/nginx/conf.d - /bees/docker_volume/nginx/file:/home/fileimage:镜像名称ports:容器的端口和宿主机的端口的映射services:文中三个service,在各自容器启动后就会自动生成别名,例如:在springboot中访问数据库,只需要通过“beesMysql:3306”就能访问。depends_on:会设置被依赖的容器启动之后,才会启动自己。例如:mysql数据库容器启动后,再启动springboot接口的容器。volumes:挂载卷,一些需要长久保存的文件,可通过宿主机中的目录,挂载到容器中,否则容器重启后会丢失。例如:数据库的数据文件;nginx的配置文件和文件服务器目录。其他自动部署为了提高开发效率,简单写了一个自动部署的脚本,直接贴脚本了:#!/bin/bashv_springboot_jar=find /bees/devops/upload/ -name "*.jar"echo “找到jar:"$v_springboot_jarv_angular_zip=find /bees/devops/upload/ -name "dist.zip"echo “找到dist:"$v_angular_zipcd /bees/conf/docker-compose downecho “关闭容器"docker rmi -f $(docker images | grep “bees-springboot” | awk ‘{print $1}’)docker rmi -f $(docker images | grep “bees-nginx-angular” | awk ‘{print $1}’)echo “删除镜像"cd /bees/devops/dockerfiles/springboot/rm -f *.jarcp $v_springboot_jar ./bees-0.0.1-SNAPSHOT.jardocker build -t bees-springboot .echo “生成springboot镜像"cd /bees/devops/dockerfiles/angular/rm -rf dist/cp $v_angular_zip ./dist.zipunzip dist.ziprm -f dist.zipdocker build -t bees-nginx-angular .echo “生成angular镜像"cd /bees/conf/docker-compose up -decho “启动容器"docker ps遇到的坑一开始在docker-compose.yml文件中写services时,每个service不是驼峰式命名,而是下划线连接,例如:bees_springboot、bees_mysql、bees_nginx_angular 。在springboot中访问数据库的别名可以,但是在nginx中,反向代理springboot接口地址时死活代理不了 bees_springboot的别名。能在bees_nginx_angular的容器中ping通bees_springboot,但是代理不了bees_springboot地址的接口,通过curl -v 查看原因,是丢失了host。最后发现,nginx默认request的header中包含“_”下划线时,会自动忽略掉。我因此把docker-compose.yml中service名称,从下划线命名都改成了驼峰式。当然也可以通过在nginx里的nginx.conf配置文件中的http部分中添加如下配置解决:underscores_in_headers on; ...

January 6, 2019 · 2 min · jiezi

spring boot 开发soap webservice

介绍spring boot web模块提供了RestController实现restful,第一次看到这个名字的时候以为还有SoapController,很可惜没有,对于soap webservice提供了另外一个模块spring-boot-starter-web-services支持。本文介绍如何在spring boot中开发soap webservice接口,以及接口如何同时支持soap和restful两种协议。soap webserviceWeb service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,既可以是soap webservice也可以是rest webservice,在rest还没出来之前,我们说webservice一般是指基于soap协议进行通信的web应用程序。在开始之前,我觉得有必要了解下soap webservice,具体的概念网上可以找到很多资料,但网上资料概念性较强,而且soap协议使用的是xml进行通信,相信xml里面一个namespace就能吓跑一大堆人,所以这里不讨论具体的soap协议细节,我想通过一个例子来说明什么是soap webservice,通过该例子,你能了解soap webservice其运作原理,当然如果你觉得你对这个已经很了解了,大可跳过本章节,本章节跟后面的内容没有任何关系。假设我们开发了一个web接口,想给别人用,我们要怎么办部署接口到服务器编写接口文档,写清楚接口是通过什么方法调的,输入参数是什么,输出参数是什么,错误时返回什么。那问题来了,我们能不能只把接口部署到服务器上,然后接口不单能提供具体的服务,而且还能自动生成一份标准的接口文档,把接口信息都记录在该文档里,如果能做到,是不是能做到"接口即文档"的目的。那么一个接口的信息包括哪些呢?接口地址接口调用方法接口输入参数接口输出参数接口出错返回信息….soap webservice里wsdl文件就是接口描述信息。核心的信息就是以上几个。第二个问题,由于Web service是一个平台独立,也就是说,使用接口的人不知道这个service是用什么技术开发的,可能是php可能是java等,但接口的参数和返回的数据都是一样的,要达到这种目的,就需要两个东西,一个是跟平台无关的数据格式,soap使用的是xml,一个是通信协议,也就是soap协议。下面就介绍如何不使用任何框架,仅通过servlet实现一个webservice。该webservice功能很简单,就是通过一个人的姓名查询这个人的详细信息。ps:servlet是java web的基础,理解servlet对理解整个java web非常重要,没写过servlet就开始用各种框架写接口就是在胡闹。1. wsdl文件准备以下wsdl文件,不要管这个文件是怎么来的,是怎么生成的,我们这次只讲原理,不谈细节,总之,你根据需求写出了这个wsdl文件。<?xml version=“1.0” encoding=“UTF-8” standalone=“no”?><wsdl:definitions xmlns:wsdl=“http://schemas.xmlsoap.org/wsdl/" xmlns:sch=“http://www.definesys.com/xml/employee" xmlns:soap=“http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns=“http://www.definesys.com/xml/employee" targetNamespace=“http://www.definesys.com/xml/employee"> <wsdl:types> <xs:schema xmlns:xs=“http://www.w3.org/2001/XMLSchema" elementFormDefault=“qualified” targetNamespace=“http://www.definesys.com/xml/employee"> <xs:element name=“EmployeeDetailRequest”> <xs:complexType> <xs:sequence> <xs:element name=“name” type=“xs:string”/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name=“EmployeeDetailResponse”> <xs:complexType> <xs:sequence> <xs:element name=“Employee” type=“tns:Employee”/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name=“Employee”> <xs:sequence> <xs:element name=“name” type=“xs:string”/> <xs:element name=“email” type=“xs:string”/> </xs:sequence> </xs:complexType></xs:schema> </wsdl:types> <wsdl:message name=“EmployeeDetailRequest”> <wsdl:part element=“tns:EmployeeDetailRequest” name=“EmployeeDetailRequest”> </wsdl:part> </wsdl:message> <wsdl:message name=“EmployeeDetailResponse”> <wsdl:part element=“tns:EmployeeDetailResponse” name=“EmployeeDetailResponse”> </wsdl:part> </wsdl:message> <wsdl:portType name=“Employee”> <wsdl:operation name=“EmployeeDetail”> <wsdl:input message=“tns:EmployeeDetailRequest” name=“EmployeeDetailRequest”> </wsdl:input> <wsdl:output message=“tns:EmployeeDetailResponse” name=“EmployeeDetailResponse”> </wsdl:output> </wsdl:operation> </wsdl:portType> <wsdl:binding name=“EmployeeSoap11” type=“tns:Employee”> <soap:binding style=“document” transport=“http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name=“EmployeeDetail”> <soap:operation soapAction=””/> <wsdl:input name=“EmployeeDetailRequest”> <soap:body use=“literal”/> </wsdl:input> <wsdl:output name=“EmployeeDetailResponse”> <soap:body use=“literal”/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name=“EmployeeService”> <wsdl:port binding=“tns:EmployeeSoap11” name=“EmployeeSoap11”> <soap:address location=“http://localhost:8081/ws-servlet/ws/employee-detail”/> </wsdl:port> </wsdl:service></wsdl:definitions>soap:address location里面端口号需要修改为servlet运行的端口号。从以下xml片段可以看出…<wsdl:binding name=“EmployeeSoap11” type=“tns:Employee”> <soap:binding style=“document” transport=“http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name=“EmployeeDetail”> <soap:operation soapAction=””/> <wsdl:input name=“EmployeeDetailRequest”> <soap:body use=“literal”/> </wsdl:input> <wsdl:output name=“EmployeeDetailResponse”> <soap:body use=“literal”/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name=“EmployeeService”> <wsdl:port binding=“tns:EmployeeSoap11” name=“EmployeeSoap11”> <soap:address location=“http://localhost:8081/ws-servlet/ws/employee-detail”/> </wsdl:port> </wsdl:service>接口名称是EmployeeDetail(wsdl:operation)接口输入参数是EmployeeDetailRequest(wsdl:input)接口输出参数是EmployeeDetailResponse(wsdl:output)接口地址是http://localhost:8081/ws-servlet/ws/employee-detail(soap:address)2. 获取wsdl文件servletpackage com.definesys.demo.servlet;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午1:45 * @history: 1.2019/1/5 created by jianfeng.zheng /public class WsdlServlet extends HttpServlet { public static final String WSDL_XML = “<?xml version="1.0" encoding="UTF-8" standalone="no"?><wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:sch="http://www.definesys.com/xml/employee" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.definesys.com/xml/employee" targetNamespace="http://www.definesys.com/xml/employee">\n” + " <wsdl:types>\n” + " <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.definesys.com/xml/employee">\n” + “\n” + " <xs:element name="EmployeeDetailRequest">\n” + " <xs:complexType>\n” + " <xs:sequence>\n” + " <xs:element name="name" type="xs:string"/>\n" + " </xs:sequence>\n" + " </xs:complexType>\n" + " </xs:element>\n" + “\n” + " <xs:element name="EmployeeDetailResponse">\n" + " <xs:complexType>\n" + " <xs:sequence>\n" + " <xs:element name="Employee" type="tns:Employee"/>\n" + " </xs:sequence>\n" + " </xs:complexType>\n" + " </xs:element>\n" + “\n” + " <xs:complexType name="Employee">\n" + " <xs:sequence>\n" + " <xs:element name="name" type="xs:string"/>\n" + " <xs:element name="email" type="xs:string"/>\n" + " </xs:sequence>\n" + " </xs:complexType>\n" + “\n” + “</xs:schema>\n” + " </wsdl:types>\n" + " <wsdl:message name="EmployeeDetailRequest">\n" + " <wsdl:part element="tns:EmployeeDetailRequest" name="EmployeeDetailRequest">\n" + " </wsdl:part>\n" + " </wsdl:message>\n" + " <wsdl:message name="EmployeeDetailResponse">\n" + " <wsdl:part element="tns:EmployeeDetailResponse" name="EmployeeDetailResponse">\n" + " </wsdl:part>\n" + " </wsdl:message>\n" + " <wsdl:portType name="Employee">\n" + " <wsdl:operation name="EmployeeDetail">\n" + " <wsdl:input message="tns:EmployeeDetailRequest" name="EmployeeDetailRequest">\n" + " </wsdl:input>\n" + " <wsdl:output message="tns:EmployeeDetailResponse" name="EmployeeDetailResponse">\n" + " </wsdl:output>\n" + " </wsdl:operation>\n" + " </wsdl:portType>\n" + " <wsdl:binding name="EmployeeSoap11" type="tns:Employee">\n" + " <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>\n" + " <wsdl:operation name="EmployeeDetail">\n" + " <soap:operation soapAction=""/>\n" + " <wsdl:input name="EmployeeDetailRequest">\n" + " <soap:body use="literal"/>\n" + " </wsdl:input>\n" + " <wsdl:output name="EmployeeDetailResponse">\n" + " <soap:body use="literal"/>\n" + " </wsdl:output>\n" + " </wsdl:operation>\n" + " </wsdl:binding>\n" + " <wsdl:service name="EmployeeService">\n" + " <wsdl:port binding="tns:EmployeeSoap11" name="EmployeeSoap11">\n" + " <soap:address location="http://localhost:8081/ws-servlet/ws/employee-detail"/>\n" + " </wsdl:port>\n" + " </wsdl:service>\n" + “</wsdl:definitions>”; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(“text/xml”); resp.getOutputStream().write(WSDL_XML.getBytes()); }}是不是很简单,是的,为了简单,我直接将wsdl文件用变量存储,我们还需要配置下web.xmlweb.xml<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns=“http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version=“3.1”> <servlet> <servlet-name>wsdl</servlet-name> <servlet-class>com.definesys.demo.servlet.WsdlServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>wsdl</servlet-name> <url-pattern>/ws/employee</url-pattern> </servlet-mapping></web-app>这样我们访问http://localhost:8080/ws/employee就能返回一个wsdl文件,也就是接口描述文件。在wsdl文件里,我们定义接口地址为http://localhost:8080/ws/employee-detail,接下来我们就要实现这个接口。3. 业务servletimport javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午2:56 * @history: 1.2019/1/5 created by jianfeng.zheng /public class EmployeeServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String response = “<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">\n” + " <SOAP-ENV:Header/>\n” + " <SOAP-ENV:Body>\n” + " <ns2:EmployeeDetailResponse xmlns:ns2="http://www.definesys.com/xml/employee">\n” + " <ns2:Employee>\n" + " <ns2:name>jianfeng</ns2:name>\n" + " <ns2:email>jianfeng.zheng@definesys.com</ns2:email>\n" + " </ns2:Employee>\n" + " </ns2:EmployeeDetailResponse>\n" + " </SOAP-ENV:Body>\n" + “</SOAP-ENV:Envelope>”; resp.getOutputStream().write(response.getBytes()); }}这里不做任何业务处理,不做xml转bean,不做bean转xml,就是这么暴力,直接返回xml,但他仍是一个soap服务,支持所有soap工具调用。将servlet配置到web.xml里web.xml<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns=“http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version=“3.1”> <servlet> <servlet-name>wsdl</servlet-name> <servlet-class>com.definesys.demo.servlet.WsdlServlet</servlet-class> </servlet> <servlet> <servlet-name>employee</servlet-name> <servlet-class>com.definesys.demo.servlet.EmployeeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>wsdl</servlet-name> <url-pattern>/ws/employee</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>employee</servlet-name> <url-pattern>/ws/employee-detail</url-pattern> </servlet-mapping></web-app>/ws/employee-detail这个地址必须和wsdl文件里定义的保持一致,不然服务无法被找到。4. 测试使用soapui测试我们的webservice,通过地址http://localhost:8081/ws-servlet/ws/employee导入wsdl文件,测试接口,返回我们在业务servlet里面写死的内容。恭喜你,你已经不依赖任何第三方包完成了一个soap webservice。当然这个只是一个玩具,但框架就是在上面的基础上进行扩展,增加wsdl文件自动生成,xml转java,java转xml,xml校验,错误处理等功能,如果你有时间,你也可以写一个soap webservice框架。代码已经上传至github,欢迎star,开始进入正题,偏的有点远。spring boot开发soap webservice1. 创建spring boot工程你可以通过spring initializr初始化spring boot工程,也可以通过inte idea的spring initializr插件进行初始化,个人推荐后面这种。2. 添加依赖添加soap webservice相关依赖包和插件,pom.xml<!–依赖–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId></dependency><dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId></dependency>…<!–插件–><plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxb2-maven-plugin</artifactId> <version>1.6</version> <executions> <execution> <id>xjc</id> <goals> <goal>xjc</goal> </goals> </execution> </executions> <configuration> <schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory> <!–<schemaFiles>employee.xsd</schemaFiles>–> <outputDirectory>${project.basedir}/src/main/java</outputDirectory> <packageName>com.definesys.tutorial.ws.type</packageName> <clearOutputDir>false</clearOutputDir> </configuration></plugin>插件jaxb2能够实现java和xml之间互转,下面是几个参数的说明schemaDirectory:xsd文件目录schemaFiles:指定schemaDirectory下的xsd文件,多个用逗号隔开,必须指定schemaDirectoryoutputDirectory:生成java文件保存目录packageName:生成java文件包路径clearOutputDir:重新生成前是否需要清空目录3. 编写xsd文件假设我们的需求是通过员工工号查询员工详细信息,根据需求编写以下xsd文件,并保存在/src/main/resources/目录下。employee.xsd<xs:schema xmlns:xs=“http://www.w3.org/2001/XMLSchema" xmlns:tns=“http://www.definesys.com/xml/employee" targetNamespace=“http://www.definesys.com/xml/employee" elementFormDefault=“qualified”> <xs:element name=“EmployeeDetailRequest”> <xs:complexType> <xs:sequence> <xs:element name=“code” type=“xs:string”/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name=“EmployeeDetailResponse”> <xs:complexType> <xs:sequence> <xs:element name=“Employee” type=“tns:Employee”/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name=“Employee”> <xs:sequence> <xs:element name=“code” type=“xs:string”/> <xs:element name=“name” type=“xs:string”/> <xs:element name=“email” type=“xs:string”/> </xs:sequence> </xs:complexType></xs:schema>4. 生成java类型文件我们需要根据xsd文件生成java类型文件,这就要借助maven插件jaxb2,打开终端运行命令mvn jaxb2:xjc,如果运行正常,就会在目录com.definesys.tutorial.ws.type下生成一堆java文件,此时文件结构如下:.├── java│ └── com│ └── definesys│ └── tutorial│ └── ws│ ├── SpringbootWsApplication.java│ └── type│ ├── Employee.java│ ├── EmployeeDetailRequest.java│ ├── EmployeeDetailResponse.java│ ├── ObjectFactory.java│ └── package-info.java└── resources ├── application.properties ├── employee.xsd ├── static └── templates5. 创建配置文件WebserviceConfig.javapackage com.definesys.tutorial.ws;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.ws.config.annotation.EnableWs;import org.springframework.ws.config.annotation.WsConfigurerAdapter;import org.springframework.ws.transport.http.MessageDispatcherServlet;import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;import org.springframework.ws.wsdl.wsdl11.Wsdl11Definition;import org.springframework.xml.xsd.SimpleXsdSchema;import org.springframework.xml.xsd.XsdSchema;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午4:46 * @history: 1.2019/1/5 created by jianfeng.zheng /@EnableWs@Configurationpublic class WebserviceConfig extends WsConfigurerAdapter { @Bean public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) { MessageDispatcherServlet servlet = new MessageDispatcherServlet(); servlet.setApplicationContext(applicationContext); servlet.setTransformWsdlLocations(true); return new ServletRegistrationBean(servlet, “/ws/”); } @Bean(name = “employee”) public Wsdl11Definition defaultWsdl11Definition(XsdSchema schema) { DefaultWsdl11Definition wsdl = new DefaultWsdl11Definition(); wsdl.setPortTypeName(“EmployeePort”); wsdl.setLocationUri("/ws/employee-detail”); wsdl.setTargetNamespace(“http://www.definesys.com/xml/employee"); wsdl.setSchema(schema); return wsdl; } @Bean public XsdSchema employeeSchema() { return new SimpleXsdSchema(new ClassPathResource(“employee.xsd”)); }}6. 创建业务服务EmployeeSoapController.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.Employee;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.ws.server.endpoint.annotation.PayloadRoot;import org.springframework.ws.server.endpoint.annotation.RequestPayload;import org.springframework.ws.server.endpoint.annotation.ResponsePayload;/** * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午4:49 * @history: 1.2019/1/5 created by jianfeng.zheng /@Endpointpublic class EmployeeSoapController { private static final String NAMESPACE_URI = “http://www.definesys.com/xml/employee"; @PayloadRoot(namespace = NAMESPACE_URI, localPart = “EmployeeDetailRequest”) @ResponsePayload public EmployeeDetailResponse getEmployee(@RequestPayload EmployeeDetailRequest request) { EmployeeDetailResponse response = new EmployeeDetailResponse(); //这里只作为演示,真正开发中需要编写业务逻辑代码 Employee employee = new Employee(); employee.setName(“jianfeng”); employee.setEmail(“jianfeng.zheng@definesys.com”); employee.setCode(request.getCode()); response.setEmployee(employee); return response; }}与RestController不一样的是,spring boot soap是根据请求报文来指定调用的函数,RestController是根据请求路径来确定。@PayloadRoot就是关键,如本次请求报文如下:<soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/" xmlns:emp=“http://www.definesys.com/xml/employee"> <soapenv:Header/> <soapenv:Body> <emp:EmployeeDetailRequest> <emp:code>?</emp:code> </emp:EmployeeDetailRequest> </soapenv:Body></soapenv:Envelope>xmlns:emp=“http://www.definesys.com/xml/employee"就是@PayloadRoot.namespace,emp:EmployeeDetailRequest对应@PayloadRoot.localPart。理解了这个其他都很好理解。7. 测试使用soapui进行测试,通过地址http://localhost:8080/ws/employee.wsdl导入wsdl文件进行测试。输入报文<soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/" xmlns:emp=“http://www.definesys.com/xml/employee"> <soapenv:Header/> <soapenv:Body> <emp:EmployeeDetailRequest> <emp:code>004</emp:code> </emp:EmployeeDetailRequest> </soapenv:Body></soapenv:Envelope>输出报文<SOAP-ENV:Envelope xmlns:SOAP-ENV=“http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header/> <SOAP-ENV:Body> <ns2:EmployeeDetailResponse xmlns:ns2=“http://www.definesys.com/xml/employee"> <ns2:Employee> <ns2:code>004</ns2:code> <ns2:name>jianfeng</ns2:name> <ns2:email>jianfeng.zheng@definesys.com</ns2:email> </ns2:Employee> </ns2:EmployeeDetailResponse> </SOAP-ENV:Body></SOAP-ENV:Envelope>同时提供soap和restful两种服务soap一般在企业内部用的比较多,做系统间的集成,restful一般用于移动应用和h5应用,如果在企业应用开发里能够同时提供两种协议的支持,将极大提高接口的复用。其实也没有想象中的那么复杂,在本例中,只需把业务逻辑部分用service实现再创建一个RestController即可,通过设计模式即可解决,不需要引入新的技术。EmployeeService.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.Employee;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.stereotype.Service;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午5:42 * @history: 1.2019/1/5 created by jianfeng.zheng /@Servicepublic class EmployeeService { public EmployeeDetailResponse getEmployee(EmployeeDetailRequest request) { EmployeeDetailResponse response = new EmployeeDetailResponse(); //这里只作为演示,真正开发中需要编写业务逻辑代码 Employee employee = new Employee(); employee.setName(“jianfeng”); employee.setEmail(“jianfeng.zheng@definesys.com”); employee.setCode(request.getCode()); response.setEmployee(employee); return response; }}EmployeeSoapController.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.Employee;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.ws.server.endpoint.annotation.Endpoint;import org.springframework.ws.server.endpoint.annotation.PayloadRoot;import org.springframework.ws.server.endpoint.annotation.RequestPayload;import org.springframework.ws.server.endpoint.annotation.ResponsePayload;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午4:49 * @history: 1.2019/1/5 created by jianfeng.zheng /@Endpointpublic class EmployeeSoapController { @Autowired private EmployeeService service; private static final String NAMESPACE_URI = “http://www.definesys.com/xml/employee"; @PayloadRoot(namespace = NAMESPACE_URI, localPart = “EmployeeDetailRequest”) @ResponsePayload public EmployeeDetailResponse getEmployee(@RequestPayload EmployeeDetailRequest request) { return service.getEmployee(request); }}EmployeeRestController.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午5:43 * @history: 1.2019/1/5 created by jianfeng.zheng */@RestController@RequestMapping(value = “/rest”)public class EmployeeRestController { @Autowired private EmployeeService service; @RequestMapping(value = “/employee-detail”, method = RequestMethod.POST) public EmployeeDetailResponse getEmployeeDetail(@RequestBody EmployeeDetailRequest request) { return service.getEmployee(request); }}测试$ curl http://localhost:8080/rest/employee-detail -X POST -d ‘{“code”:“004”}’ -H “Content-Type: application/json”{ “employee”: { “code”: “004”, “name”: “jianfeng”, “email”: “jianfeng.zheng@definesys.com” }}这样就实现了soap和rest同时提供的目的。本文代码已提交至gitlab欢迎star相关参考文档https://spring.io/guides/gs/producing-web-service/https://github.com/wls1036/tutorial-springboot-soaphttps://github.com/wls1036/pure-ws-servlet ...

January 5, 2019 · 5 min · jiezi

SpringBoot系列教程之RedisTemplate 基本配置说明文档

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题在Spring的应用中,redis可以算是基础操作了。那么想要玩转redis,我们需要知道哪些知识点呢?redis配置,默认,非默认,集群,多实例,连接池参数等redis读写操作,RedisTemplate的基本使用姿势几种序列化方式对比本篇博文为redis系列的开篇,将介绍最基本的配置原文链接: 181029-SpringBoot高级篇Redis之基本配置I. redis基本配置1. 默认配置最简单的使用其实开箱即可用,添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>本机启动redis,一切采用默认的配置 (host:127.0.0.1, port:6379, 无密码)然后就可以愉快的玩耍了,可以直接注入redisTemplate实例,进行各种读写操作@SpringBootApplicationpublic class Application { public Application(RedisTemplate<String, String> redisTemplate) { redisTemplate.opsForValue().set(“hello”, “world”); String ans = redisTemplate.opsForValue().get(“hello”); Assert.isTrue(“world”.equals(ans)); } public static void main(String[] args) { SpringApplication.run(Application.class, args); }}2. 自定义配置参数前面是默认的配置参数,在实际的使用中,一般都会修改这些默认的配置项,如果我的应用中,只有一个redis,那么完全可以只修改默认的配置参数修改配置文件: application.ymlspring: redis: host: 127.0.0.1 port: 6379 password: database: 0 lettuce: pool: max-active: 32 max-wait: 300ms max-idle: 16 min-idle: 8使用和前面没有什么区别,直接通过注入RedisTemplate来操作即可,需要额外注意的是设置了连接池的相关参数,需要额外引入依赖<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId></dependency>3. 多redis配置依赖多个不同的redis,也就是说我的项目需要从多个redis实例中获取数据,这种时候,就不能直接使用默认的,需要我们自己来声明ConnectionFactory和 RedisTemplate配置如下spring: redis: host: 127.0.0.1 port: 6379 password: lettuce: pool: max-active: 32 max-wait: 300 max-idle: 16 min-idle: 8 database: 0 local-redis: host: 127.0.0.1 port: 6379 database: 0 password: lettuce: pool: max-active: 16 max-wait: 100 max-idle: 8 min-idle: 4对应的配置类,采用Lettuce,基本设置如下,套路都差不多,先读取配置,初始化ConnectionFactory,然后创建RedisTemplate实例,设置连接工厂@Configurationpublic class RedisAutoConfig { @Bean public LettuceConnectionFactory defaultLettuceConnectionFactory(RedisStandaloneConfiguration defaultRedisConfig, GenericObjectPoolConfig defaultPoolConfig) { LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(100)) .poolConfig(defaultPoolConfig).build(); return new LettuceConnectionFactory(defaultRedisConfig, clientConfig); } @Bean public RedisTemplate<String, String> defaultRedisTemplate( LettuceConnectionFactory defaultLettuceConnectionFactory) { RedisTemplate<String, String> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(defaultLettuceConnectionFactory); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean @ConditionalOnBean(name = “localRedisConfig”) public LettuceConnectionFactory localLettuceConnectionFactory(RedisStandaloneConfiguration localRedisConfig, GenericObjectPoolConfig localPoolConfig) { LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(100)) .poolConfig(localPoolConfig).build(); return new LettuceConnectionFactory(localRedisConfig, clientConfig); } @Bean @ConditionalOnBean(name = “localLettuceConnectionFactory”) public RedisTemplate<String, String> localRedisTemplate(LettuceConnectionFactory localLettuceConnectionFactory) { RedisTemplate<String, String> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(localLettuceConnectionFactory); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Configuration @ConditionalOnProperty(name = “host”, prefix = “spring.local-redis”) public static class LocalRedisConfig { @Value("${spring.local-redis.host:127.0.0.1}") private String host; @Value("${spring.local-redis.port:6379}") private Integer port; @Value("${spring.local-redis.password:}") private String password; @Value("${spring.local-redis.database:0}") private Integer database; @Value("${spring.local-redis.lettuce.pool.max-active:8}") private Integer maxActive; @Value("${spring.local-redis.lettuce.pool.max-idle:8}") private Integer maxIdle; @Value("${spring.local-redis.lettuce.pool.max-wait:-1}") private Long maxWait; @Value("${spring.local-redis.lettuce.pool.min-idle:0}") private Integer minIdle; @Bean public GenericObjectPoolConfig localPoolConfig() { GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxTotal(maxActive); config.setMaxIdle(maxIdle); config.setMinIdle(minIdle); config.setMaxWaitMillis(maxWait); return config; } @Bean public RedisStandaloneConfiguration localRedisConfig() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); config.setHostName(host); config.setPassword(RedisPassword.of(password)); config.setPort(port); config.setDatabase(database); return config; } } @Configuration public static class DefaultRedisConfig { @Value("${spring.redis.host:127.0.0.1}") private String host; @Value("${spring.redis.port:6379}") private Integer port; @Value("${spring.redis.password:}") private String password; @Value("${spring.redis.database:0}") private Integer database; @Value("${spring.redis.lettuce.pool.max-active:8}") private Integer maxActive; @Value("${spring.redis.lettuce.pool.max-idle:8}") private Integer maxIdle; @Value("${spring.redis.lettuce.pool.max-wait:-1}") private Long maxWait; @Value("${spring.redis.lettuce.pool.min-idle:0}") private Integer minIdle; @Bean public GenericObjectPoolConfig defaultPoolConfig() { GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxTotal(maxActive); config.setMaxIdle(maxIdle); config.setMinIdle(minIdle); config.setMaxWaitMillis(maxWait); return config; } @Bean public RedisStandaloneConfiguration defaultRedisConfig() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); config.setHostName(host); config.setPassword(RedisPassword.of(password)); config.setPort(port); config.setDatabase(database); return config; } }}测试类如下,简单的演示下两个template的读写@SpringBootApplicationpublic class Application { public Application(RedisTemplate<String, String> localRedisTemplate, RedisTemplate<String, String> defaultRedisTemplate) throws InterruptedException { // 10s的有效时间 localRedisTemplate.delete(“key”); localRedisTemplate.opsForValue().set(“key”, “value”, 100, TimeUnit.MILLISECONDS); String ans = localRedisTemplate.opsForValue().get(“key”); System.out.println(“value”.equals(ans)); TimeUnit.MILLISECONDS.sleep(200); ans = localRedisTemplate.opsForValue().get(“key”); System.out.println(“value”.equals(ans) + " >> false ans should be null! ans=[" + ans + “]”); defaultRedisTemplate.opsForValue().set(“key”, “value”, 100, TimeUnit.MILLISECONDS); ans = defaultRedisTemplate.opsForValue().get(“key”); System.out.println(ans); } public static void main(String[] args) { SpringApplication.run(Application.class, args); }}上面的代码执行演示如下上面的演示为动图,抓一下重点:注意 localRedisTemplate, defaultRedisTemplate 两个对象不相同(看debug窗口后面的@xxx)同样两个RedisTemplate的ConnectionFactory也是两个不同的实例(即分别对应前面配置类中的两个Factory)执行后输出的结果正如我们预期的redis操作塞值,马上取出没问题失效后,再查询,返回null最后输出异常日志,提示如下Description:Parameter 0 of method redisTemplate in org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration required a single bean, but 2 were found: - defaultLettuceConnectionFactory: defined by method ‘defaultLettuceConnectionFactory’ in class path resource [com/git/hui/boot/redis/config/RedisAutoConfig.class] - localLettuceConnectionFactory: defined by method ’localLettuceConnectionFactory’ in class path resource [com/git/hui/boot/redis/config/RedisAutoConfig.class]Action:Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed上面表示说有多个ConnectionFactory存在,然后创建默认的RedisTemplate就不知道该选择哪一个了,有两种方法方法一:指定默认的ConnectionFactory借助@Primary来指定默认的连接工厂,然后在使用工程的时候,通过@Qualifier注解来显示指定,我需要的工厂是哪个(主要是localRedisTemplate这个bean的定义,如果不加,则会根据defaultLettuceConnectionFactory这个实例来创建Redis连接了)@Bean@Primarypublic LettuceConnectionFactory defaultLettuceConnectionFactory(RedisStandaloneConfiguration defaultRedisConfig, GenericObjectPoolConfig defaultPoolConfig) { // …}@Beanpublic RedisTemplate<String, String> defaultRedisTemplate( @Qualifier(“defaultLettuceConnectionFactory”) LettuceConnectionFactory defaultLettuceConnectionFactory) { // ….}@Bean@ConditionalOnBean(name = “localRedisConfig”)public LettuceConnectionFactory localLettuceConnectionFactory(RedisStandaloneConfiguration localRedisConfig, GenericObjectPoolConfig localPoolConfig) { // …}@Bean@ConditionalOnBean(name = “localLettuceConnectionFactory”)public RedisTemplate<String, String> localRedisTemplate( @Qualifier(“localLettuceConnectionFactory”) LettuceConnectionFactory localLettuceConnectionFactory) { // …}方法二:忽略默认的自动配置类既然提示的是org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration类加载bean冲突,那么就不加载这个配置即可@SpringBootApplication@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class})public class Application { // …}II. 其他0. 项目工程:spring-boot-demomodule: 120-redis-config1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 28, 2018 · 3 min · jiezi

SpringBoot 日志框架

日志框架日志框架就是为了更好的记录日志使用的,记录日志是为了我们在工作中可以更好的查找相对应的问题,也算是以中留痕操作!以前我们刚开始学习的时候都是用System.out.println()去在控制台记录,怎么说呢?这种方式伴随着我们很长时间,之后我们就遇到了断点调试的方式,逐渐不在使用System.out.println()进行调试,但是你别忘记,那是一种记录不管是否有用,你都应该去记录!市面上的日志框架;JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j….日志门面 (日志的抽象层)日志实现JCL(Jakarta Commons Logging) SLF4j(Simple Logging Facade for Java) jboss-loggingLog4j JUL(java.util.logging) Log4j2 LogbackSpringBoot:底层是Spring框架,Spring框架默认是用JCL;‘ <==SpringBoot选用 SLF4j和logback;==>1.SLF4j使用1.1 如何在系统中使用SLF4jSLF4J的官方网站手册以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法;给系统里面导入slf4j的jar和 logback的实现jar使用方式如下:import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class HelloWorld { public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.info(“Hello World”); }}每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件;1.2 历史遗留问题我们接触过的框架使用的日志框架都有所不同,因此,统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出?1.3 slf4j统一“天下”将系统中其他日志框架先排除出去;用中间包来替换原有的日志框架;我们导入slf4j其他的实现;2. Spring Boot 日志配置SpringBoot使用它来做日志功能:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId></dependency>SpringBoot底层也是使用slf4j+logback的方式进行日志记录SpringBoot也把其他的日志都替换成了slf4j中间替换包?@SuppressWarnings(“rawtypes”)public abstract class LogFactory { static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = “http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j"; static LogFactory logFactory = new SLF4JLogFactory();如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉示例:Spring框架用的是commons-logging;<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions></dependency>pringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;3. 日志的使用Spring Boot 默认给我们已经配置好了日志,测试如下://记录器Logger logger = LoggerFactory.getLogger(getClass());@Testpublic void contextLoads() { //System.out.println(); //日志的级别; //由低到高 trace<debug<info<warn<error //可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效 logger.trace(“这是trace日志…”); logger.debug(“这是debug日志…”); //SpringBoot默认给我们使用的是info级别的,没有指定级别的就用SpringBoot默认规定的级别;root级别 logger.info(“这是info日志…”); logger.warn(“这是warn日志…”); logger.error(“这是error日志…”);}3.1 日志格式说明 日志输出格式: %d表示日期时间, %thread表示线程名, %-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息, %n是换行符 –> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%nSpringBoot修改日志的默认配置logging.level.com.hanpang=trace# com.hanpang是说明的包名#logging.path=# 不指定路径在当前项目下生成springboot.log日志# 可以指定完整的路径;#logging.file=G:/springboot.log# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件logging.path=/spring/log# 在控制台输出的日志的格式logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n# 指定文件中日志输出的格式logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%nlogging.filelogging.pathExampleDescription(none)(none) 只在控制台输出指定文件名(none)my.log输出日志到my.log文件(none)指定目录/var/log输出到指定目录的 spring.log 文件中3.2 指定配置给类路径下放上每个日志框架自己的配置文件即可,SpringBoot就不使用他默认配置的了Logging SystemCustomizationLogbacklogback-spring.xml, logback-spring.groovy, logback.xml or logback.groovyLog4j2log4j2-spring.xml or log4j2.xmlJDK (Java Util Logging)logging.propertieslogback.xml:直接就被日志框架识别了;一般情况不推荐(推荐)logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能<springProfile name=“staging”> <!– configuration to be enabled when the “staging” profile is active –> 可以指定某段配置只在某个环境下生效</springProfile>示例代码<appender name=“stdout” class=“ch.qos.logback.core.ConsoleAppender”> <!– 日志输出格式: %d表示日期时间, %thread表示线程名, %-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息, %n是换行符 –> <layout class=“ch.qos.logback.classic.PatternLayout”> <springProfile name=“dev”> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} —-> [%thread] —> %-5level %logger{50} - %msg%n</pattern> </springProfile> <springProfile name="!dev”> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern> </springProfile> </layout> </appender>如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误:no applicable action for [springProfile]4. 切换日志框架可以按照slf4j的日志适配图,进行相关的切换;slf4j+log4j的方式,pom.xml配置如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <exclusion> <artifactId>log4j-over-slf4j</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId></dependency>如果切换为log4j2,pom.xml配置如下:log4j2配置的参考文章log4j2的配置说明<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-logging</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId></dependency>5.“坑”logging.path和logging.file不可以同时配置,同时配置也只有logging.file起效配置logging.path将会在指定文件夹下面生成spring.log文件,文件名字无法控制配置logging.file,如果只是文件名如:demo.log只会在项目的根目录下生成指定文件名的日志文件,,如果想控制日志路径,可以选择完整路径,如:E:\demo\demo.log接下来看看自定义配置文件,这个就要方便很多了,还是喜欢自定义配置文件的方式在src/main/resources下面新建文件logback.xml这个也是spring boot默认的配置文件名,如果需要自定义文件名,如:logback-test.xml需要在application.properties添加配置================但是,我们习惯使用logback-spring.xml==========================logging.config=classpath:logback-test.xmlspring boot默认载入的相关配置文件,详见jar包;spring-boot-1...RELEASE.jar下面org/springframework/boot/logging/logback/详细文件:base.xml //基础包,引用了下面所有的配置文件console-appender.xml //控制台输出配置defaults.xml //默认的日志文件配置file-appender.xml //文件输出配置附录logback-spring.xml 配置说明<?xml version=“1.0” encoding=“UTF-8”?><configuration> <!–定义日志文件的存储地址 勿在 LogBack的配置中使用相对路径 –> <property name=“LOG_HOME” value="/tmp/log" /> <!– 控制台输出 –> <appender name=“STDOUT” class=“ch.qos.logback.core.ConsoleAppender”> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern> </encoder> </appender> <!– 按照每天生成日志文件 –> <appender name=“FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <FileNamePattern>${LOG_HOME}/logs/smsismp.log.%d{yyyy-MM-dd}.log</FileNamePattern> <!–日志文件保留天数 –> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <!–格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 –> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern> </encoder> <!–日志文件最大的大小 –> <triggeringPolicy class=“ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy”> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <!– 日志输出级别 –> <root level=“INFO”> <appender-ref ref=“STDOUT” /> <appender-ref ref=“FILE” /> </root> <!– 定义各个包的详细路径,继承root宝的值 –> <logger name=“com.hry.spring.log” level=“INFO” /> <logger name=“com.hry.spring” level=“TRACE” /> <!– 此值由 application.properties的spring.profiles.active=dev指定–> <springProfile name=“dev”> <!–定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 –> <property name=“LOG_HOME” value="/tmp/log" /> <logger name=“org.springboot.sample” level=“DEBUG” /> </springProfile> <springProfile name=“pro”> <!–定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 –> <property name=“LOG_HOME” value="/home" /> <logger name=“org.springboot.sample2” level=“INFO” /> </springProfile> </configuration>部分说明appender name=“STDOUT”: 日志打印到控制台appender name=“FILE”: 日志按日打印到文件中,最多保留MaxHistory天,每个文件大水为MaxFileSizeencoder:定义输出格式<root level=“INFO”>: 定义根logger,通过appender-ref指定前方定义的appender<logger name=“com.hry.spring.log” level=“INFO” />:在继承root的logger上对com.hry.spring.log包日志作特殊处理<springProfile name=“dev”>: 定义profile的值,只有特定profile的情况下,此间定义的内容才启作用application.properties 启动dev配置信息 server.port=8080 spring.profiles.active=devspring.profiles.active指定本次启动的active的值是什么。本次是dev,则logback-spring.xml里<springProfile name=“dev”>的内容启作用import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplicationpublic class LogApplication { private static final Logger log = LoggerFactory.getLogger(LogApplication.class); public static void main(String[] args) { String str1 = “string1”; String str2 = “string2”; log.info(“Begin Start {}…{}”, str1, str2); SpringApplication.run(LogApplication.class, args); log.info(“Stop …”); }}logback-spring.xml 其他的写法<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/base.xml” /> <logger name=“org.springframework.web” level=“INFO”/> <logger name=“org.springboot.sample” level=“TRACE” /> <!– 测试环境+开发环境. 多个使用逗号隔开. –> <springProfile name=“test,dev”> <logger name=“org.springframework.web” level=“DEBUG”/> <logger name=“org.springboot.sample” level=“DEBUG” /> <logger name=“com.example” level=“DEBUG” /> </springProfile> <!– 生产环境. –> <springProfile name=“prod”> <logger name=“org.springframework.web” level=“ERROR”/> <logger name=“org.springboot.sample” level=“ERROR” /> <logger name=“com.example” level=“ERROR” /> </springProfile></configuration>这里说明一下:1) 引入的base.xml是Spring Boot的日志系统预先定义了一些系统变量的基础配置文件2) 在application.properties中设置环境为prod,则只会打印error级别日志3) 如果在application.properties中定义了相同的配置,则application.properties的日志优先级更高可以在内部进行引用<?xml version=“1.0” encoding=“utf-8”?><configuration scan=“true” scanPeriod=“10 seconds”> <!– 文件输出格式 –> <property name=“pattern” value="%d{yyyy-MM-dd HH:mm:ss.SSS} -%5p ${PID:-} [%15.15t] %-40.40logger{39} : %m%n" /> <property name=“charsetEncoding” value=“UTF-8” /> <!–<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>–> <!–控制台日志–> <appender name=“console” class=“ch.qos.logback.core.ConsoleAppender”> <encoder> <pattern>${pattern}</pattern> <charset>UTF-8</charset> </encoder> </appender> <appender name=“file” class=“ch.qos.logback.core.FileAppender”> <file>./logback/logfile.log</file> <append>true</append> <encoder> <pattern>${pattern}</pattern> <charset>${charsetEncoding}</charset> </encoder> </appender> <appender name=“dailyRollingFileAppender” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <File>./logback/log.log</File> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <!– daily rollover –> <FileNamePattern>logback.%d{yyyy-MM-dd_HH}.log</FileNamePattern> <!– keep 30 days’ worth of history –> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <Pattern>${pattern}</Pattern> </encoder> </appender> <logger name=“org.springframework.web” level=“debug”/> <!– show parameters for hibernate sql 专为 Hibernate 定制 –> <logger name=“org.hibernate.type.descriptor.sql.BasicBinder” level=“TRACE” /> <logger name=“org.hibernate.type.descriptor.sql.BasicExtractor” level=“DEBUG” /> <logger name=“org.hibernate.SQL” level=“DEBUG” /> <logger name=“org.hibernate.engine.QueryParameters” level=“DEBUG” /> <logger name=“org.hibernate.engine.query.HQLQueryPlan” level=“DEBUG” /> <!–myibatis log configure–> <logger name=“com.apache.ibatis” level=“TRACE”/> <logger name=“java.sql.Connection” level=“DEBUG”/> <logger name=“java.sql.Statement” level=“DEBUG”/> <logger name=“java.sql.PreparedStatement” level=“DEBUG”/> <root level=“debug”> <appender-ref ref=“console”/> <appender-ref ref=“dailyRollingFileAppender”/> <appender-ref ref=“file”/> </root></configuration> ...

December 27, 2018 · 3 min · jiezi

关于线上环境springboot突然文件上传失败问题

一、问题描述本来跑的好好的文件上传代码,突然有一天出现了以下问题:what???怎么突然间就出现无效的文件上传路径了呢?二、原因在参考了一些文章后,才知道这是属于linux的一个坑1.spring boot的应用服务在启动的时候,会生成在操作系统的/tmp目录下生成一个Tomcat.的文件目录,用于"java.io.tmpdir"文件流操作2.程序对文件的操作时:会生成临时文件,暂存在临时文件中;lunix 系统的tmpwatch 命令会删除10天未使用的临时文件;长时间不操作,导致/tmp下面的tomcat临时文件目录被删除,且删除的文件不可恢复,上传文件时获取不到文件目录,报错三、解决方法1:重启服务,但是不建议在生产环境使用。2:增加服务配置,服务启动时指定参数 -DbaseDir= 自定义baseDir。3 注入bean,手动配置一个临时目录就可以了/* * 文件上传临时路径 */ @Bean MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setLocation("/usr/app/ad/tmp"); return factory.createMultipartConfig();}

December 26, 2018 · 1 min · jiezi

SpringBoot应用篇之借助Redis实现排行榜功能

更多Spring文章,欢迎点击 一灰灰Blog-Spring专题在一些游戏和活动中,当涉及到社交元素的时候,排行榜可以说是一个很常见的需求场景了,就我们通常见到的排行榜而言,会提供以下基本功能全球榜单,对所有用户根据积分进行排名,并在榜单上展示前多少个人排名,用户查询自己所在榜单的位置,并获知周边小伙伴的积分,方便自己比较和超越实时更新,用户的积分实时更改,榜单也需要实时更新上面可以说是一个排行榜需要实现的几个基本要素了,正好我们刚讲到了redis这一节,本篇则开始实战,详细描述如何借助redis来实现一份全球排行榜<!– more –>I. 方案设计在进行方案设计之前,先模拟一个真实的应用场景,然后进行辅助设计与实现1. 业务场景说明以前一段时间特别????的跳一跳这个小游戏进行说明,假设我们这个游戏用户遍布全球,因此我们要设计一个全球的榜单,每个玩家都会根据自己的战绩在排行榜中获取一个排名,我们需要支持全球榜单的查询,自己排位的查询这两种最基本的查询场景;此外当我的分数比上一次的高时,我需要更新我的积分,重新获得我的排名;此外也会有一些高级的统计,比如哪个分段的人数最多,什么分段是瓶颈点,再根据地理位置计算平均分等等本篇博文主要内容将放在排行榜的设计与实现上;至于高级的功能实现,后续有机会再说2. 数据结构因为排行榜的功能比较简单了,也不需要什么复杂的结构设计,也没有什么复杂的交互,因此我们需要确认的无非就是数据结构 + 存储单元存储单元表示排行榜中每一位上应该持有的信息,一个最简单的如下// 用来表明具体的用户long userId;// 用户在排行榜上的排名long rank;// 用户的历史最高积分,也就是排行榜上的积分long score;数据结构排行榜,一般而言都是连续的,借此我们可以联想到一个合适的数据结构LinkedList,好处在于排名变动时,不需要数组的拷贝上图演示,当一个用户积分改变时,需要向前遍历找到合适的位置,插入并获取新的排名, 在更新和插入时,相比较于ArrayList要好很多,但依然有以下几个缺陷问题1:用户如何获取自己的排名?使用LinkedList在更新插入和删除的带来优势之外,在随机获取元素的支持会差一点,最差的情况就是从头到尾进行扫描问题2:并发支持的问题?当有多个用户同时更新score时,并发的更新排名问题就比较突出了,当然可以使用jdk中类似写时拷贝数组的方案上面是我们自己来实现这个数据结构时,会遇到的一些问题,当然我们的主题是借助redis来实现排行榜,下面则来看下,利用redis可以怎么简单的支持我们的需求场景3. redis使用方案这里主要使用的是redis的ZSET数据结构,带权重的集合,下面分析一下可能性set: 集合确保里面元素的唯一性权重:这个可以看做我们的score,这样每个元素都有一个score;zset:根据score进行排序的集合从zset的特性来看,我们每个用户的积分,丢到zset中,就是一个带权重的元素,而且是已经排好序的了,只需要获取元素对应的index,就是我们预期的排名II. 功能实现再具体的实现之前,可以先查看一下redis中zset的相关方法和操作姿势:SpringBoot高级篇Redis之ZSet数据结构使用姿势我们主要是借助zset提供的一些方法来实现排行榜的需求,下面的具体方法设计中,也会有相关说明0. 前提准备首先准备好redis环境,spring项目搭建好,然后配置好redisTemplate/** * Created by @author yihui in 15:05 18/11/8. /public class DefaultSerializer implements RedisSerializer<Object> { private final Charset charset; public DefaultSerializer() { this(Charset.forName(“UTF8”)); } public DefaultSerializer(Charset charset) { Assert.notNull(charset, “Charset must not be null!”); this.charset = charset; } @Override public byte[] serialize(Object o) throws SerializationException { return o == null ? null : String.valueOf(o).getBytes(charset); } @Override public Object deserialize(byte[] bytes) throws SerializationException { return bytes == null ? null : new String(bytes, charset); }}@Configurationpublic class AutoConfig { @Bean(value = “selfRedisTemplate”) public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate redis = new StringRedisTemplate(); redis.setConnectionFactory(redisConnectionFactory); // 设置redis的String/Value的默认序列化方式 DefaultSerializer stringRedisSerializer = new DefaultSerializer(); redis.setKeySerializer(stringRedisSerializer); redis.setValueSerializer(stringRedisSerializer); redis.setHashKeySerializer(stringRedisSerializer); redis.setHashValueSerializer(stringRedisSerializer); redis.afterPropertiesSet(); return redis; }}1. 用户上传积分上传用户积分,然而zset中有一点需要注意的是其排行是根据score进行升序排列,这个就和我们实际的情况不太一样了;为了和实际情况一致,可以将score取反;另外一个就是排行默认是从0开始的,这个与我们的实际也不太一样,需要+1/* * 更新用户积分,并获取最新的个人所在排行榜信息 * * @param userId * @param score * @return /public RankDO updateRank(Long userId, Float score) { // 因为zset默认积分小的在前面,所以我们对score进行取反,这样用户的积分越大,对应的score越小,排名越高 redisComponent.add(RANK_PREFIX, String.valueOf(userId), -score); Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId)); return new RankDO(rank + 1, score, userId);}上面的实现,主要利用了zset的两个方法,一个是添加元素,一个是查询排名,对应的redis操作方法如下,@Resource(name = “selfRedisTemplate”)private StringRedisTemplate redisTemplate; /* * 添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能; zadd * * @param key * @param value * @param score /public void add(String key, String value, double score) { redisTemplate.opsForZSet().add(key, value, score);} /* * 判断value在zset中的排名 zrank * * 积分小的在前面 * * @param key * @param value * @return /public Long rank(String key, String value) { return redisTemplate.opsForZSet().rank(key, value);}2. 获取个人排名获取个人排行信息,主要就是两个一个是排名一个是积分;需要注意的是当用户没有积分时(即没有上榜时),需要额外处理/* * 获取用户的排行榜位置 * * @param userId * @return /public RankDO getRank(Long userId) { // 获取排行, 因为默认是0为开头,因此实际的排名需要+1 Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId)); if (rank == null) { // 没有排行时,直接返回一个默认的 return new RankDO(-1L, 0F, userId); } // 获取积分 Double score = redisComponent.score(RANK_PREFIX, String.valueOf(userId)); return new RankDO(rank + 1, Math.abs(score.floatValue()), userId);}上面的封装中,除了使用前面的获取用户排名之外,还有获取用户积分/* * 查询value对应的score zscore * * @param key * @param value * @return /public Double score(String key, String value) { return redisTemplate.opsForZSet().score(key, value);}3. 获取个人周边用户积分及排行信息有了前面的基础之后,这个就比较简单了,首先获取用户的个人排名,然后查询固定排名段的数据即可private List<RankDO> buildRedisRankToBizDO(Set<ZSetOperations.TypedTuple<String>> result, long offset) { List<RankDO> rankList = new ArrayList<>(result.size()); long rank = offset; for (ZSetOperations.TypedTuple<String> sub : result) { rankList.add(new RankDO(rank++, Math.abs(sub.getScore().floatValue()), Long.parseLong(sub.getValue()))); } return rankList;}/* * 获取用户所在排行榜的位置,以及排行榜中其前后n个用户的排行信息 * * @param userId * @param n * @return /public List<RankDO> getRankAroundUser(Long userId, int n) { // 首先是获取用户对应的排名 RankDO rank = getRank(userId); if (rank.getRank() <= 0) { // fixme 用户没有上榜时,不返回 return Collections.emptyList(); } // 因为实际的排名是从0开始的,所以查询周边排名时,需要将n-1 Set<ZSetOperations.TypedTuple<String>> result = redisComponent.rangeWithScore(RANK_PREFIX, Math.max(0, rank.getRank() - n - 1), rank.getRank() + n - 1); return buildRedisRankToBizDO(result, rank.getRank() - n);}看下上面的实现,获取用户排名之后,就可以计算要查询的排名范围[Math.max(0, rank.getRank() - n - 1), rank.getRank() + n - 1]其次需要注意的如何将返回的结果进行封装,上面写了个转换类,主要起始排行榜信息4. 获取topn排行榜上面的理解之后,这个就很简答了/* * 获取前n名的排行榜数据 * * @param n * @return */public List<RankDO> getTopNRanks(int n) { Set<ZSetOperations.TypedTuple<String>> result = redisComponent.rangeWithScore(RANK_PREFIX, 0, n - 1); return buildRedisRankToBizDO(result, 1);}III. 测试小结首先准备一个测试脚本,批量的插入一下积分,用于后续的查询更新使用public class RankInitTest { private Random random; private RestTemplate restTemplate; @Before public void init() { random = new Random(); restTemplate = new RestTemplate(); } private int genUserId() { return random.nextInt(1024); } private double genScore() { return random.nextDouble() * 100; } @Test public void testInitRank() { for (int i = 0; i < 30; i++) { restTemplate.getForObject(“http://localhost:8080/update?userId=” + genUserId() + “&score=” + genScore(), String.class); } }}1. 测试上面执行完毕之后,排行榜中应该就有三十条数据,接下来我们开始逐个接口测试,首先获取top10排行对应的rest接口如下@RestControllerpublic class RankAction { @Autowired private RankListComponent rankListComponent; @GetMapping(path = “/topn”) public List<RankDO> showTopN(int n) { return rankListComponent.getTopNRanks(n); }}接下来我们挑选第15名,获取对应的排行榜信息@GetMapping(path = “/rank”)public RankDO queryRank(long userId) { return rankListComponent.getRank(userId);}首先我们从redis中获取第15名的userId,然后再来查询然后尝试修改下他的积分,改大一点,将score改成80分,则会排到第五名@GetMapping(path = “/update”)public RankDO updateScore(long userId, float score) { return rankListComponent.updateRank(userId, score);}最后我们查询下这个用户周边2个的排名信息@GetMapping(path = “/around”)public List<RankDO> around(long userId, int n) { return rankListComponent.getRankAroundUser(userId, n);}2. 小结上面利用redis的zset实现了排行榜的基本功能,主要借助下面三个方法range 获取范围排行信息score 获取对应的scorerange 获取对应的排名虽然实现了基本功能,但是问题还是有不少的上面的实现,redis的复合操作,原子性问题由原子性问题导致并发安全问题性能怎么样需要测试III. 其他0. 项目工程:spring-boot-demomodule源码: spring-case/120-redis-ranklist1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

December 25, 2018 · 3 min · jiezi

Profile配置和加载配置文件

Profile配置1.Profile是什么很多时候,我们项目在开发环境和生成环境的环境配置是不一样的,例如,数据库配置,在开发的时候,我们一般用测试数据库,而在生产环境的时候,我们是用正式的数据,这时候,我们可以利用profile在不同的环境下配置用不同的配置文件或者不同的配置。spring boot允许你通过命名约定按照一定的格式(application-{profile}.properties)来定义多个配置文件,然后通过在application.properyies通过spring.profiles.active来具体激活一个或者多个配置文件,如果没有没有指定任何profile的配置文件的话,spring boot默认会启动application-default.properties。2.基于properties文件类型假如有开发、测试、生产三个不同的环境,需要定义三个不同环境下的配置。你可以另外建立3个环境下的配置文件:applcation.propertiesapplication-dev.propertiesapplication-test.propertiesapplication-prod.properties然后在applcation.properties文件中指定当前的环境: spring.profiles.active=test这时候读取的就是application-test.properties文件。server.port=8001# 激活哪个配置文件spring.profiles.active=devspring.profiles.include=prod可以包含其他的配置文件信息3.基于yml文件类型只需要一个applcation.yml文件就能搞定,推荐此方式。spring: profiles: active: prod—spring: profiles: dev server: port: 8080 —spring: profiles: test server: port: 8081 —spring.profiles: prodspring.profiles.include: - proddb - prodmq server: port: 8082 —spring: profiles: proddb db: name: mysql —spring: profiles: prodmq mq: address: localhost此时读取的就是prod的配置,prod包含proddb,prodmq,此时可以读取proddb,prodmq下的配置也可以同时激活三个配置spring.profiles.active: prod,proddb,prodmq3.基于Java代码在JAVA配置代码中也可以加不同Profile下定义不同的配置文件,@Profile注解只能组合使用@Configuration和@Component注解。@Configuration@Profile(“prod”)public class ProductionConfiguration { // …}4.指定Profile不适用配置文件,而是在启动的时候进行指定的写法4.1 main方法启动方式:// 在IDE Arguments里面添加–spring.profiles.active=prod优先级高于在配置文件里面的激活的4.2 JVM启动方式-Dspring.profiles.active=dev4.3 插件启动方式spring-boot:run -Drun.profiles=prod4.4 jar运行方式java -jar xx.jar –spring.profiles.active=prod除了在配置文件和命令行中指定Profile,还可以在启动类中写死指定,通过SpringApplication.setAdditionalProfiles方法public void setAdditionalProfiles(String… profiles) { this.additionalProfiles = new LinkedHashSet<String>(Arrays.asList(profiles));}配置文件加载位置spring boot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:file:./config/ - 优先级最高(项目根路径下的config)file:./ - 优先级第二 -(项目根路径下)classpath:/config/ - 优先级第三(项目resources/config下)classpath:/ - 优先级第四(项目resources根目录)重要的规则,跟我们之前学过的不太一样高优先级配置会覆盖低优先级配置多个配置文件互补比如,两个同名文件里面有相同的配置,相同的配置会被高优先级的配置覆盖A配置优先级大于B配置server: port: 8080B配置优先级小于A配置server: port: 8081 context-path: /hanpang项目启动后访问地址为:http://127.0.0.1:8080/hanpang,这就是所谓的互补通过配置spring.config.location来改变默认配置java -jar demo-xxx.jar –spring.config.location=C:/application.properties这对于运维来说非常方便,在不破坏原配置情况下轻松修改少量配置就可以达到想要的效果外部配置加载顺序来自于网路,个人没有进行相关的测试SpringBoot也可以从以下位置加载配置:优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置。命令行参数所有的配置都可以在命令行上进行指定;多个配置用空格分开; –配置项=值java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar –server.port=8087 –server.context-path=/abc来自java:comp/env的JNDI属性Java系统属性(System.getProperties())操作系统环境变量RandomValuePropertySource配置的random.*属性值jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件.jar包外部的application.properties或application.yml(不带spring.profile)配置文件jar包内部的application.properties或application.yml(不带spring.profile)配置文件由jar包外向jar包内进行寻找,优先加载待profile的,再加载不带profile的。@Configuration注解类上的@PropertySource通过SpringApplication.setDefaultProperties指定的默认属性加载配置文件方式参考官网地址 ...

December 25, 2018 · 1 min · jiezi

springboot 整合 elasticsearch 失败

Failed to instantiate [org.elasticsearch.client.transport.TransportClient]: Factory method ’elasticsearchClient’ threw exception; nested exception is java.lang.IllegalStateException: availableProcessors is already set to [4], rejecting [4]@SpringBootApplicationpublic class SpringBootExampleApplication { public static void main(String[] args) { System.setProperty(“es.set.netty.runtime.available.processors”,“false”); SpringApplication.run(SpringbootexampleApplication.class, args); }}参考链接 elasticsearch5.6.1.集成springboot 遇到的坑

December 25, 2018 · 1 min · jiezi

springboot新版本(2.1.0)、springcloud新版本(Greenwich.M1)实现链路追踪的一些坑

主要问题 由于springboot新版本(2.1.0)、springcloud新版本(Greenwich.M1)实现链路追踪sleuth+zipkin的一些“新特性”,使得我在实现sleuth+zipkin的过程上踩了不少坑。 在springboot1.X版本的时候,实现链路追踪服务需要用户自己实现client以及server,通常在server服务端需要引入各种各样的包(spring-cloud-sleuth-stream,以及支持zipkin的一些相关依赖包等等); 但在spring cloud新版本实现链路追踪sleuth+zipkin的方式上已经不再需要自己再去实现一个server服务端(集成sleuth+zipkin),而是由zinkin官方提供了一个现成的zipkin-server.jar,或者是一个docker镜像,用户可以下载并通过命令进行启动它,用户可以通一些配置来确定sleuth收集到信息后传输到zipkin之间采用http,还是通过rabbit/kafka的方式。在新的版本下,用户只需要关注slenth-client选用何种传输方式(http或mq(rabbit/kafka),如果选择http,则在配置中指明base-url;如果选择mq,则在配置指明相关消息中间件的相关信息host/port/username/password…),至于zipkin的信息storage问题,则由zipkin-server要负责,可以通过zipkin-server.jar 配置一些具体的参数来启动。(下面会细讲)ps:这不是教程贴,这主要是解决一些问题的一些方法,不会有详细的实现过程,但为了简明我会贴上部分代码。背景 最近开始实习了,老大让我自学一下sc(spring cloud),学就学嘛,也不是难事。看完spring cloud的全家桶,老大说让我重点了解一下它的链路追踪服务,后期会有这方面的任务安排给我做,所以呢我就重点关注这一方面,打算自己做个demo练练手,看了网上的教程,膨胀的我选择了个最新的版本,结果发现就这么掉坑里了。。。版本按照惯例,先说下springboot跟spring cloud的版本springboot:2.1.0springcloud:Greenwich.M1个人建议新手不要过分追求新版本,旧版本的还是够用的,比springboot 2.6.0搭配sringcloud Finchley SR2还是挺稳的,如果真的要探索新版本你会发现这里面的坑实在是踩不完,基本要花个一两天才能让自己从坑里跳出去,这样频繁踩坑会让新手很容易放弃~~~ps:不要问我为什么知道。。。主要问题闲话扯完了,可以进入正题了一共四个服务eureka-serverzipkin-server:新版本的zipkin服务端,负责接受sleuth发送过来的数据,完成处理、存储、建立索引,并且提供了一个可视化的ui数据分析界面。需要的同学话可以直接在github上下载https://github.com/openzipkin…嗯就是这两个家伙下面两个是两个服务eureka-server服务注册中心,这个实现我就不讲了,网上搜一大把,各个版本实现基本都是一致的,并不存在版本更新跨度极大的情况。而且这里我把它是打包成一个jar包,在需要的时候直接用java -jar XXX.jar 直接启动至于product跟order(也即实际场景下各种种样的服务A、B、C…)order服务只有一个接口/test,去调用product的接口这里的productclient就是使用feignf去调用order的/product/list接口product只有一个接口/product/list,查找所有商品的列表简单的来说,这里的场景就是order服务–(去调用)–>product服务说完场景后,贴一下这两个服务的相关配置信息(order跟producet的配置基本上是相同的)application.ymlspring: application: #服务名 name: product #由于业务逻辑需要操作数据库,所以这里配置了mysql的一些信息 datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 url: jdbc:mysql://127.0.0.1:3306/sc_sell?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai jpa: show-sql: true #重点 zipkin: #base-url:当你设置sleuth-cli收集信息后通过http传输到zinkin-server时,需要在这里配置 base-url: http://localhost:9411 enabled: true sleuth: sampler: #收集追踪信息的比率,如果是0.1则表示只记录10%的追踪数据,如果要全部追踪,设置为1(实际场景不推荐,因为会造成不小的性能消耗) probability: 1eureka: client: service-url: #注册中心地址 defaultZone: http://localhost:8999/eureka/logging: level: #这个是设置feign的一个日志级别,key-val的形式设置 org.springframework.cloud.openfeign: debug说完配置信息,就该讲一下依赖了,很简单,client实现链路追踪只需要添加一个依赖spring-cloud-starter-zipkin。就是这个 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>其实这些都是基础操作,是吧,那么来点进阶的。在sleuth-cli跟zipkin-server之间插入一个消息中间件rabbitmq/kafka,这里我举例中只使用rabbitmq来实现将链路追踪的数据存储到DB上,目前zipkin暂时只支持mysql/elasticsearch,这里我使用mysql如果你是刚开始学习sc,给你去实现的话,你肯定会开始打开浏览器开始搜索教程。结果你会发现,大部分博客上都是以前版本的实现方式,一些较旧会让你自己实现一个zipkin-server(我怀疑他们的版本是1.x),你会发现很郁闷,因为这跟你想象的不太一样啊。继续找,终于在茫茫帖子中,找到了一篇是关于springboot2.0.X版本的实现链路追踪的教程,这时候你会兴奋,终于找到靠谱一点的啊,喜出望外有木有啊,但是,事情还没完,它会让你在客户端依赖下面这个依赖包 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-stream</artifactId> </dependency>结果你会发现,你在依赖它的时候,其实是依赖不了,为什么?因为版本的问题,什么?你跟我说你的pom文件没报错啊,但是,你打开idea右边的maven插件看一下这真的是一个巨坑,我一直不明白是怎么回事,直到有一次,我打开了这个页面,花了我一天的时间去摸索是什么原因造成的集成rabbitmq失败,真的是被安排得明明白白最后,豪无头绪的我,继续在网上查找一些springboot2.x版本的一些链路追踪的教程,在搜索了一个下午,我突然想起,诶不对,我应该直接去官网看它的官方教程的啊。。。虽然都英文,大不了我用chrome自带的翻译工具翻译一下咯。结果就立马打开spring的官网,选择了最新的版本,进去找了一下,还真的让我找到了!!!不得不说官方文档的重要性!https://cloud.spring.io/sprin…

December 25, 2018 · 1 min · jiezi

SpringBoot注入数据的方式

关于注入数据说明1.不通过配置文件注入数据通过@Value将外部的值动态注入到Bean中,使用的情况有:注入普通字符串注入操作系统属性注入表达式结果注入其他Bean属性:注入Student对象的属性name注入文件资源注入URL资源辅助代码package com.hannpang.model;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Component(value = “st”)//对student进行实例化操作public class Student { @Value(“悟空”) private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }}测试@Value的代码package com.hannpang.model;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Component;@Componentpublic class SimpleObject { @Value(“注入普通字符串”) private String normal; //关于属性的KEY可以查看System类说明 @Value("#{systemProperties[‘java.version’]}")//–>使用了SpEL表达式 private String systemPropertiesName; // 注入操作系统属性 @Value("#{T(java.lang.Math).random()80}")//获取随机数 private double randomNumber; //注入表达式结果 @Value("#{1+2}") private double sum; //注入表达式结果 1+2的求和 @Value(“classpath:os.yaml”) private Resource resourceFile; // 注入文件资源 @Value(“http://www.baidu.com”) private Resource testUrl; // 注入URL资源 @Value("#{st.name}") private String studentName; //省略getter和setter方法 @Override public String toString() { return “SimpleObject{” + “normal=’” + normal + ‘'’ + “, systemPropertiesName=’” + systemPropertiesName + ‘'’ + “, randomNumber=” + randomNumber + “, sum=” + sum + “, resourceFile=” + resourceFile + “, testUrl=” + testUrl + “, studentName=’” + studentName + ‘'’ + ‘}’; }}Spring的测试代码package com.hannpang;import com.hannpang.model.SimpleObject;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;@RunWith(SpringRunner.class)@SpringBootTestpublic class Demo04BootApplicationTests { @Autowired private SimpleObject so; @Test public void contextLoads() { System.out.println(so); }}运行结果为:SimpleObject{normal=‘注入普通字符串’, systemPropertiesName=‘1.8.0_172’, randomNumber=56.631954541947266, sum=3.0, resourceFile=class path resource [os.yaml], testUrl=URL [http://www.baidu.com], studentName=‘悟空’}2.通过配置文件注入数据通过@Value将外部配置文件的值动态注入到Bean中。配置文件主要有两类:application.properties、application.yaml application.properties在spring boot启动时默认加载此文件自定义属性文件。自定义属性文件通过@PropertySource加载。@PropertySource可以同时加载多个文件,也可以加载单个文件。如果相同第一个属性文件和第二属性文件存在相同key,则最后一个属性文件里的key启作用。加载文件的路径也可以配置变量,如下文的${anotherfile.configinject},此值定义在第一个属性文件config.properties在application.properties中加入如下测试代码app.name=一步教育在resources下面新建第一个属性文件config.properties内容如下book.name=西游记anotherfile.configinject=system在resources下面新建第二个属性文件config_system.properties内容如下我的目的是想system的值使用第一个属性文件中定义的值book.name.author=吴承恩下面通过@Value(“${app.name}”)语法将属性文件的值注入bean属性值,详细代码见:package com.hannpang.test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.PropertySource;import org.springframework.core.env.Environment;import org.springframework.stereotype.Component;@Component@PropertySource(value = {“classpath:config.properties”,“classpath:config_${anotherfile.configinject}.properties”})public class LoadPropsTest { @Value("${app.name}") private String appName; // 这里的值来自application.properties,spring boot启动时默认加载此文件 @Value("${book.name}") private String bookName; // 注入第一个配置外部文件属性 @Value("${book.name.author}") private String author; // 注入第二个配置外部文件属性 @Autowired private Environment env; // 注入环境变量对象,存储注入的属性值 //省略getter和setter方法 public void setAuthor(String author) { this.author = author; } @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append(“bookName=”).append(bookName).append("\r\n") .append(“author=”).append(author).append("\r\n") .append(“appName=”).append(appName).append("\r\n") .append(“env=”).append(env).append("\r\n") // 从eniroment中获取属性值 .append(“env=”).append(env.getProperty(“book.name.author”)).append("\r\n"); return sb.toString(); }}测试代码package com.hannpang;import com.hannpang.model.SimpleObject;import com.hannpang.test.LoadPropsTest;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;@RunWith(SpringRunner.class)@SpringBootTestpublic class Demo04BootApplicationTests { @Autowired private LoadPropsTest lpt; @Test public void loadPropertiesTest() { System.out.println(lpt); }}运行结果为:bookName=西游记author=吴承恩appName=一步教育env=StandardEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[ConfigurationPropertySourcesPropertySource {name=‘configurationProperties’}, MapPropertySource {name=‘Inlined Test Properties’}, MapPropertySource {name=‘systemProperties’}, OriginAwareSystemEnvironmentPropertySource {name=‘systemEnvironment’}, RandomValuePropertySource {name=‘random’}, OriginTrackedMapPropertySource {name=‘applicationConfig: [classpath:/application.properties]’}, ResourcePropertySource {name=‘class path resource [config_system.properties]’}, ResourcePropertySource {name=‘class path resource [config.properties]’}]}env=吴承恩3. #{…}和${…}的区别演示A.${…}的用法{}里面的内容必须符合SpEL表达式,通过@Value(“${app.name}”)可以获取属性文件中对应的值,但是如果属性文件中没有这个属性,则会报错。可以通过赋予默认值解决这个问题,如@Value("${app.name:胖先森}")部分代码// 如果属性文件没有app.name,则会报错// @Value("${app.name}")// private String name;// 使用app.name设置值,如果不存在则使用默认值@Value("${app.name:胖先森}")private String name;B.#{…}的用法部分代码直接演示// SpEL:调用字符串Hello World的concat方法@Value("#{‘Hello World’.concat(’!’)}")private String helloWorld;// SpEL: 调用字符串的getBytes方法,然后调用length属性@Value("#{‘Hello World’.bytes.length}")private String helloWorldbytes;C.#{…}和${…}混合使用${…}和#{…}可以混合使用,如下文代码执行顺序:通过${server.name}从属性文件中获取值并进行替换,然后就变成了 执行SpEL表达式{‘server1,server2,server3’.split(‘,’)}。// SpEL: 传入一个字符串,根据",“切分后插入列表中, #{}和${}配置使用(注意单引号,注意不能反过来${}在外面,#{}在里面)@Value(”#{’${server.name}’.split(’,’)}")private List<String> servers;在上文中在#{}外面,${}在里面可以执行成功,那么反过来是否可以呢${}在外面,#{}在里面,如代码// SpEL: 注意不能反过来${}在外面,#{}在里面,这个会执行失败@Value("${#{‘HelloWorld’.concat(’’)}}")private List<String> servers2;答案是不能。因为spring执行${}是时机要早于#{}。在本例中,Spring会尝试从属性中查找#{‘HelloWorld’.concat(‘’)},那么肯定找到,由上文已知如果找不到,然后报错。所以${}在外面,#{}在里面是非法操作D.用法总结#{…} 用于执行SpEl表达式,并将内容赋值给属性${…} 主要用于加载外部属性文件中的值#{…} 和&dollar;{…} 可以混合使用,但是必须#{}外面,&dollar;{}在里面4.@Value获取值和@ConfigurationProperties获取值比较 @ConfigurationProperties@Value功能批量注入配置文件中的属性一个个指定松散绑定(松散语法)支持不支持SpEL不支持支持JSR303数据校验支持不支持复杂类型封装支持不支持配置文件yml还是properties他们都能获取到值;如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value;如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties;关于数据校验的部分代码@Component@ConfigurationProperties(prefix = “person”)@Validatedpublic class Person { //lastName必须是邮箱格式 @Email private String lastName;5. @ImportResource引入配置文件不推荐的使用方式Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别;想让Spring的配置文件生效,加载进来;@ImportResource标注在一个配置类上@ImportResource(locations = {“classpath:beans.xml”})导入Spring的配置文件让其生效编写配置文件信息<?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.xsd"> <bean id=“helloService” class=“com.hanpang.springboot.service.HelloService”></bean></beans>大概了解就好,我们基本上不使用这种方式6.@Configuration注解SpringBoot推荐给容器中添加组件的方式;推荐使用全注解的方式1、配置类@Configuration作用于类上,相当于一个xml配置文件2、使用@Bean给容器中添加组件,作用于方法上/* * @Configuration:指明当前类是一个配置类;就是来替代之前的Spring配置文件 * * 在配置文件中用<bean><bean/>标签添加组件 * <bean id=“helloService” class=“com.hanpang.springboot.service.HelloService”></bean> /@Configurationpublic class MyAppConfig { //将方法的返回值添加到容器中;容器中这个组件默认的id就是方法名 @Bean public HelloService helloService02(){ System.out.println(“配置类@Bean给容器中添加组件了…”); return new HelloService(); }}使用Bean注入太麻烦,我们更加喜欢使用扫描的方式import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration; import com.wx.dao.IUserDao;import com.wx.dao.UserDaoImpl; //通过该注解来表明该类是一个Spring的配置,相当于一个传统的ApplicationContext.xml@Configuration//相当于配置文件里面的<context:component-scan/>标签,扫描这些包下面的类的注解@ComponentScan(basePackages=“com.hanpang.dao,com.hanpang.service”)public class SpringConfig { // 通过该注解来表明是一个Bean对象,相当于xml中的<bean> //bean的id值默认是方法名userDao / @Bean public HelloService helloService02(){ System.out.println(“配置类@Bean给容器中添加组件了…”); return new HelloService(); } */}附录随机数${random.value}、${random.int}、${random.long}${random.int(10)}、${random.int[1024,65536]} ...

December 23, 2018 · 2 min · jiezi

spring cache 配置缓存存活时间

Spring Cache @Cacheable本身不支持key expiration的设置,以下代码可自定义实现Spring Cache的expiration,针对Redis、SpringBoot2.0。直接上代码:@Service@Configurationpublic class CustomCacheMng{ private Logger logger = LoggerFactory.getLogger(this.getClass()); // 指明自定义cacheManager的bean name @Cacheable(value = “test”,key = “‘obj1’",cacheManager = “customCacheManager”) public User cache1(){ User user = new User().setId(1); logger.info(“1”); return user; } @Cacheable(value = “test”,key = “‘obj2’”) public User cache2(){ User user = new User().setId(1); logger.info(“2”); return user; } // 自定义的cacheManager,实现存活2天 @Bean(name = “customCacheManager”) public CacheManager cacheManager( RedisTemplate<?, ?> redisTemplate) { RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(2)); return new RedisCacheManager(writer, config); } // 提供默认的cacheManager,应用于全局 @Bean @Primary public CacheManager defaultCacheManager( RedisTemplate<?, ?> redisTemplate) { RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); return new RedisCacheManager(writer, config); }} ...

December 21, 2018 · 1 min · jiezi

YAML语法简易入门

YAML 语法个人感觉这篇文档属于"搬运工"性质可以查看官方或者其他的博客资料,会有一大堆的内容YAML语言的设计参考了JSON,XML和SDL等语言。YAML 强调以数据为中心,简洁易读,编写简单。有意思的命名:AML全称是”YAML Ain’t a Markup Language”(YAML不是一种置标语言)的递归缩写。 在开发的这种语言时,YAML 的意思其实是:”Yet Another Markup Language”(仍是一种置标语言)。语法特点大小写敏感通过缩进表示层级关系禁止使用tab缩进,只能使用空格键 (个人感觉这条最重要)缩进的空格数目不重要,只要相同层级左对齐即可使用#表示注释支持的数据结构对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)纯量(scalars):单个的、不可再分的值双引号和单引号的区分双引号"":不会转义字符串里面的特殊字符,特殊字符作为本身想表示的意思。name: “123\n123”—————————输出: 123 换行 123如果不加引号将会转义特殊字符,当成字符串处理值的写法1.字符串使用”或”“或不使用引号value0: ‘hello World!‘value1: “hello World!“value2: hello World!2.布尔值true或false表示。3.数字12 #整数 014 # 八进制整数 0xC #十六进制整数 13.4 #浮点数 1.2e+34 #指数 .inf空值 #无穷大4.空值null或~表示5.日期使用 iso-8601 标准表示日期date: 2018-01-01t16:59:43.10-05:00在springboot中yaml文件的时间格式 date: yyyy/MM/dd HH:mm:ss6.强制类型转换(了解)YAML 允许使用个感叹号!,强制转换数据类型,单叹号通常是自定义类型,双叹号是内置类型。money: !!str123date: !Booleantrue内置类型列表!!int # 整数类型 !!float # 浮点类型 !!bool # 布尔类型 !!str # 字符串类型 !!binary # 也是字符串类型 !!timestamp # 日期时间类型 !!null # 空值 !!set # 集合 !!omap,!!pairs # 键值列表或对象列表!!seq # 序列,也是列表 !!map # 键值表7.对象(重点)Map(属性和值)(键值对)的形式: key:(空格)v :表示一堆键值对,空格不可省略。car: color: red brand: BMW一行写法car:{color: red,brand: BMW}相当于JSON格式:{“color”:“red”,“brand”:“BMW”}8.数组一组连词线开头的行,构成一个数组。brand: - audi - bmw - ferrari一行写法brand: [audi,bmw,ferrari]相当于JSON[“auri”,“bmw”,“ferrari”]9.文本块|:使用|标注的文本内容缩进表示的块,可以保留块中已有的回车换行value: | hello world!输出结果:hello 换行 world!+表示保留文字块末尾的换行,-表示删除字符串末尾的换行。value: |hellovalue: |-hellovalue: |+hello输出:hello\n hello hello\n\n(有多少个回车就有多少个\n)注意 “|” 与 文本之间须另起一行>:使用 > 标注的文本内容缩进表示的块,将块中回车替换为空格,最终连接成一行value: > helloworld!输出:hello 空格 world!注意 “>” 与 文本之间的空格10.锚点与引用使用 & 定义数据锚点(即要复制的数据),使用 * 引用锚点数据(即数据的复制目的地)name: &a yamlbook: abooks: - java - a - python输出book: yaml 输出books:[java,yaml,python]注意引用部分不能追加内容配置文件注入数据/* * 将配置文件中配置的每一个属性的值,映射到这个组件中 * @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定; * prefix = “person”:配置文件中哪个下面的所有属性进行一一映射 * * 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能; * */@Component //实例化@ConfigurationProperties(prefix = “person”)//yaml或者properties的前缀public class Person { private String name; private Integer age; private Boolean flag; private Date birthday; private Map<String,Object> maps; private List<Object> tempList; private Dog dog; //省略getter和setter以及toString方法我们可以导入配置文件处理器,以后编写配置就有提示了,@ConfigurationPropertiesIDE会提示打开在线的帮助文档,配置依赖如下:<!–导入配置文件处理器,配置文件进行绑定就会有提示–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional></dependency>application.yaml文件person: name: 胖先森 age: 18 flag: false birthday: 2018/12/19 20:21:22 #Spring Boot中时间格式 maps: {bookName: “西游记”,author: ‘吴承恩’} tempList: - 红楼梦 - 三国演义 - 水浒传 dog: dogName: 大黄 dogAge: 4在test中进行测试如下@RunWith(SpringRunner.class)@SpringBootTestpublic class Demo03BootApplicationTests { @Autowired private Person p1; @Test public void contextLoads() { System.out.println(p1); }}输出结果为:Person{name=‘胖先森’, age=18, flag=false, birthday=Wed Dec 19 20:21:22 CST 2018, maps={bookName=西游记, author=吴承恩}, tempList=[红楼梦, 三国演义, 水浒传], dog=Dog{dogName=‘大黄’, dogAge=4}}application.properties文件person123.name=刘备person123.age=20person123.birthday=2018/12/19 20:21:22person123.maps.bookName=水浒传person123.maps.author=罗贯中person123.temp-list=一步教育,步步为赢person123.dog.dogName=小白person123.dog.dogAge=5java代码修改前缀@Component //实例化@ConfigurationProperties(prefix = “person123”)//yaml或者properties的前缀public class Person { private String name; private Integer age; private Boolean flag; private Date birthday; private Map<String,Object> maps; private List<Object> tempList; private Dog dog; //省略getter和setter以及toString方法在test中进行测试如下@RunWith(SpringRunner.class)@SpringBootTestpublic class Demo03BootApplicationTests { @Autowired private Person p1; @Test public void contextLoads() { System.out.println(p1); }}输出结果为:Person{name=‘����’, age=20, flag=null, birthday=Wed Dec 19 20:21:22 CST 2018, maps={bookName=ˮ䰴�, author=�޹���}, tempList=[һ������, ����ΪӮ], dog=Dog{dogName=‘��’, dogAge=5}}属性文件中文乱码问题 ...

December 19, 2018 · 2 min · jiezi

SpringBoot ActiveMq JmsTemplate 异步发送、非持久化

ActiveMq事务ActiveMq事务的作用就是在发送、接收处理消息过程中,如果出现问题,可以回滚。ActiveMq异步/同步发送以下摘抄自https://blog.csdn.net/songhai… 同步发送:消息生产者使用持久(persistent)传递模式发送消息的时候,Producer.send() 方法会被阻塞,直到 broker 发送一个确认消息给生产者(ProducerAck),这个确认消息暗示broker已经成功接收到消息并把消息保存到二级存储中。异步发送如果应用程序能够容忍一些消息的丢失,那么可以使用异步发送。异步发送不会在受到 broker 的确认之前一直阻塞 Producer.send 方法。当发送方法在一个事务上下文中时,被阻塞的是 commit 方法而不是 send 方法。commit 方法成功返回意味着所有的持久消息都以被写到二级存储中。想要使用异步,在brokerURL中增加 jms.alwaysSyncSend=false&jms.useAsyncSend=true如果设置了alwaysSyncSend=true系统将会忽略useAsyncSend设置的值都采用同步 1) 当alwaysSyncSend=false时,“NON_PERSISTENT”(非持久化)、事务中的消息将使用“异步发送” 2) 当alwaysSyncSend=false时,如果指定了useAsyncSend=true,“PERSISTENT”类型的消息使用异步发送。如果useAsyncSend=false,“PERSISTENT”类型的消息使用同步发送。总结:默认情况(alwaysSyncSend=false,useAsyncSend=false),非持久化消息、事务内的消息均采用异步发送;对于持久化消息采用同步发送。 jms.sendTimeout:发送超时时间,默认等于0,如果jms.sendTimeout>0将会忽略(alwaysSyncSend、useAsyncSend、消息是否持久化)所有的消息都是用同步发送!官方连接:http://activemq.apache.org/as…配置使用异步发送方式1.在连接上配置cf = new ActiveMQConnectionFactory(“tcp://locahost:61616?jms.useAsyncSend=true”);2.通过ConnectionFactory((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true);3.通过connection((ActiveMQConnection)connection).setUseAsyncSend(true);SpringBoot JMS实现异步发送1.如果在配置中使用了连接池,那么SpringBoot默认会使用PooledConnectionFactory,ActiveMQConnectionFactory的useAsyncSend默认会true。使用连接池配置如下activemq: in-memory: true broker-url: tcp://127.0.0.1:61616 pool: enabled: true max-connections: 5 user: password:2.修改JmsTemplate 默认参数JmsTemplate template = new JmsTemplate(pooledConnectionFactory);//设备为true,deliveryMode, priority, timeToLive等设置才会起作用template.setExplicitQosEnabled(true);//设为非持久化模式template.setDeliveryMode(DeliveryMode.NON_PERSISTENT);完整代码如下:@Slf4j@Configurationpublic class ActiveConfig { /** * 配置用于异步发送的非持久化JmsTemplate / @Autowired @Bean @Primary public JmsTemplate asynJmsTemplate(PooledConnectionFactory pooledConnectionFactory) { JmsTemplate template = new JmsTemplate(pooledConnectionFactory); template.setExplicitQosEnabled(true); template.setDeliveryMode(DeliveryMode.NON_PERSISTENT); log.info(“jsmtemplate ————->sessionTransacted:{}",template.isSessionTransacted()); log.info(“jsmtemplate ————->ExplicitQosEnabled:{}",template.isExplicitQosEnabled()); return template; } /* * 配置用于同步发送的持久化JmsTemplate */ @Autowired @Bean public JmsTemplate synJmsTemplate(PooledConnectionFactory pooledConnectionFactory) { JmsTemplate template = new JmsTemplate(pooledConnectionFactory); log.info(“jsmtemplate ————->sessionTransacted:{}",template.isSessionTransacted()); log.info(“jsmtemplate ————->ExplicitQosEnabled:{}",template.isExplicitQosEnabled()); return template; }//如果对于SpringBoot自动生成的PooledConnectionFactory需要调优,可以自己生PooledConnectionFactory调优参数// private PooledConnectionFactory getPooledConnectionFactory(String userName,String password,String brokerURL) {// ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(userName,password,brokerURL);// ActiveMQPrefetchPolicy activeMQPrefetchPolicy = new ActiveMQPrefetchPolicy();// activeMQConnectionFactory.setPrefetchPolicy(activeMQPrefetchPolicy);// PooledConnectionFactory pooledConnectionFactory = new PooledConnectionFactory(activeMQConnectionFactory);// pooledConnectionFactory.setMaxConnections(5);// return pooledConnectionFactory;// } ...

December 19, 2018 · 1 min · jiezi

Spring Boot 集成 MyBatis和 SQL Server实践

文章共 509字,阅读大约需要 2分钟 !概 述Spring Boot工程集成 MyBatis来实现 MySQL访问的示例我们见过很多,而最近用到了微软的 SQL Server数据库,于是本文则给出一个完整的 Spring Boot + MyBatis + SQL Server 的工程示例。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站工程搭建新建 Spring Boot工程pom.xml 中添加 MyBatis和 SQL Server相关的依赖<!–for mybatis–><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version></dependency><!–for SqlServer–><dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>sqljdbc4</artifactId> <version>4.0</version></dependency>配置 application.properties这里同样主要是对于 MyBatis 和 SQL Server连接相关的配置server.port=89# mybatis 配置mybatis.type-aliases-package=cn.codesheep.springbt_mybatis_sqlserver.entitymybatis.mapper-locations=classpath:mapper/*.xmlmybatis.configuration.map-underscore-to-camel-case=true## ————————————————-## SqlServer 配置spring.datasource.url=jdbc:sqlserver://xxxx:1433;databasename=MingLispring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriverspring.datasource.username=xxxxspring.datasource.password=xxxx建立 SQL Server数据表和实体类首先在 SQL Server数据库中新建数据表 user_test作为测试用表DROP TABLE [demo].[user_test]GOCREATE TABLE [dbo].[user_test] ([user_id] int NOT NULL ,[user_name] varchar(50) NOT NULL ,[sex] tinyint NOT NULL ,[created_time] varchar(50) NOT NULL )GO然后在我们的工程中对应建立的 User实体类其字段和实际数据表的字段一一对应public class User { private Long userId; private String userName; private Boolean sex; private String createdTime; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Boolean getSex() { return sex; } public void setSex(Boolean sex) { this.sex = sex; } public String getCreatedTime() { return createdTime; } public void setCreatedTime(String createdTime) { this.createdTime = createdTime; }}Mybatis Mapper映射配置MyBatis映射配置的 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=“cn.codesheep.springbt_mybatis_sqlserver.mapper.UserMapper”> <resultMap id=“userMap” type=“cn.codesheep.springbt_mybatis_sqlserver.entity.User”> <id property=“userId” column=“user_id” javaType=“java.lang.Long”></id> <result property=“userName” column=“user_name” javaType=“java.lang.String”></result> <result property=“sex” column=“sex” javaType=“java.lang.Boolean”></result> <result property=“createdTime” column=“created_time” javaType=“java.lang.String”></result> </resultMap> <select id=“getAllUsers” resultMap=“userMap”> select * from user_test </select> <insert id=“addUser” parameterType=“cn.codesheep.springbt_mybatis_sqlserver.entity.User”> insert into user_test ( user_id, user_name, sex, created_time ) values ( #{userId}, #{userName}, #{sex}, #{createdTime} ) </insert> <delete id=“deleteUser” parameterType=“cn.codesheep.springbt_mybatis_sqlserver.entity.User”> delete from user_test where user_name = #{userName} </delete></mapper>与此同时,这里也给出对应 XML的 DAO接口public interface UserMapper { List<User> getAllUsers(); int addUser( User user ); int deleteUser( User user );}为了试验起见,这里给出了 增 / 删 / 查 三个数据库操作动作。编写 Service 和测试Controller上面这些准备工作完成之后,接下来编写数据库 CRUD的 Service类@Service@Primarypublic class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public List<User> getAllUsers() { return userMapper.getAllUsers(); } @Override public int addUser(User user) { SimpleDateFormat form = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); user.setCreatedTime( form.format(new Date()) ); return userMapper.addUser( user ); } @Override public int deleteUser(User user) { return userMapper.deleteUser( user ); }}这里的 Service功能同样主要关于数据表的 增 / 删 / 查 三个数据库操作动作。对照着上面的Service,我们编写一个对应接口测试的Controller@RestControllerpublic class UserController { @Autowired private IUserService userService; @RequestMapping(value = “/getAllUser”, method = RequestMethod.GET) public List<User> getAllUser() { return userService.getAllUsers(); } @RequestMapping(value = “/addUser”, method = RequestMethod.POST) public int addUser( @RequestBody User user ) { return userService.addUser( user ); } @RequestMapping(value = “/deleteUser”, method = RequestMethod.POST) public int deleteUser( @RequestBody User user ) { return userService.deleteUser( user ); }}实验测试插入数据依次用 POSTMAN通过 Post /addUser接口插入三条数据:{“userId”:1,“userName”:“刘能”,“sex”:true}{“userId”:2,“userName”:“赵四”,“sex”:false}{“userId”:3,“userName”:“王大拿”,“sex”:true}插入完成后去 SQL Server数据库里看一下数据插入情况如下:查询数据调用 Get /getAllUser接口,获取刚插入的几条数据删除数据调用 Post /deleteUser 接口,可以通过用户名来删除对应的用户后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊我的半年技术博客之路 ...

December 18, 2018 · 2 min · jiezi

从一道简单的“SpringBoot配置文件”相关面试题,我就能知道你的水平

面试要套路,也要技巧。别被背题目的兄弟们给忽悠了。【你来发挥】你比较喜欢什么技术,哪一种最熟?一般自信的面试官都喜欢问这个问题,这次面试的小伙比较年轻,咱也装回B,不然都对不起自己。答: 我比较喜欢Spring,比较有趣。目的: 希望应聘者能够有广度且有深度。如果最感兴趣的是Spring本身,而不是其上的解决方案,那顶多会承担被分解后的编码工作。巧了,咱也熟。【工作经验】SpringBoot相比较SpringMVC,有什么优缺点?答: 有很多方面。觉得最好的就是不用写那么多配置文件了,直接写个注解,通过自动配置,就完成了初始化。目的: 说什么无所谓,主要看有没有总结能力。判断是否用过早期的Spring版本,经历过版本更新更能了解软件开发之痛,接受新知识会考虑兼容和迭代。【实现原理】我想要SpringBoot自动读取我的自定义配置,该做哪些事情?答: 写一个相应的starter目的: 判断是否了解和写过Spring Boot Starter,主要是META-INF目录下的spring.factories文件和AutoConfiguration。了解AOP更佳。【烟幕弹】配置文件是yml格式,这种格式你喜欢么?答: 比较喜欢properties格式,感觉yml格式的配置文件缩进比较难处理。比如当我从网上拷贝一些别人长长的配置文件,可能要花较多时间整理文件格式。目的 此问题没有具体的意图,主要是过渡用。【动手能力】这么喜欢properties方式,能够写一段代码,将yml翻译成properties么? 要是回答相反则反着来。目的 通过简单的伪代码,判断应聘者的动手能力和编码风格。是喜欢问题抽象化还是喜欢立刻动手去写。我希望回答能够有条理,而且能够考虑各种异常情况,比如把自己判断不了的配置交给用户处理;比如空格和<TAB>的处理。【提示】提示一下,你用什么数据结构进行存储?目的 假如应聘者在一段时间内不能有任何产出,会给出简单的提示。找准了存储结构,你就基本完成了工作,此问题还判断了应聘者的培养成本和价值。【基础算法】哦,是树结构,遍历方式是怎样的?前序,后序,中序?目的 判断是否有基础的算法知识。做工程先不要求会什么动态规划或者贪心算法,但起码的数据结构是要了解的。【基础知识】你用到了Map?Java中如何做可排序的Map?目的 是否对java的基础集合类熟悉,期望回答TreeMap,如果回答上来,可能会追问它是什么数据结构(红黑树)。【知识广度】你还接触过哪些配置方式?比较喜欢那种?目的 了解应聘者的知识广度,说不出来也无所谓,了解的多会加分。比如ini、cfg、json、toml、序列化等。【项目规模】我想要把我的配置放在云端,比如数据库密码等,改怎么做?目的 是否了解SpringBoot的组件SpringConfig,或者了解一些其他的开源组件如携程的apollo等。【知识广度】我想要配置文件改动的时候,所有机器自动更新,该怎么办?目的 了解是否知晓常用的同步方式。有两种:一种是定时去轮询更新;一种是使用zk或者etcd这种主动通知的组件。【实现细节】Spring是如何进行配置文件刷新的?目的 这个可真是没写过就真不知道了,主要是org.springframework.cloud.context.scope.refresh.RefreshScope这个类【架构能力】现在我想要将配置文件分发到一部分机器,也就是带有版本的灰度概念,你该如何处理?目的 如果能够从网关、微服务约定,后台操作原型方面去多方位描述一下,更佳。这样筛选的小伙伴,都很棒!能力多少,心中有数。

December 11, 2018 · 1 min · jiezi

SpringBoot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床

Github 地址:https://github.com/Snailclimb/springboot-integration-examples(SpringBoot和其他常用技术的整合,可能是你遇到的讲解最详细的学习案例,力争新手也能看懂并且能够在看完之后独立实践。基于最新的 SpringBoot2.0+,是你学习SpringBoot 的最佳指南。) ,欢迎各位 Star。笔主很早就开始用阿里云OSS 存储服务当做自己的图床了。如果没有用过阿里云OSS 存储服务或者不是很了解这个东西的可以看看官方文档,我这里就不多做介绍了。阿里云对象存储 OSS文档,:https://help.aliyun.com/product/31815.html?spm=a2c4g.11186623.6.540.4e401c62EyJK5T本篇文章会介绍到 SpringBoot 整合阿里云OSS 存储服务实现文件上传下载以及简单的查看。其实今天将的应该算的上是一个简单的小案例了,涉及到的知识点还算是比较多。一 开发前的准备1.1 前置知识具有 Java 基础以及SpringBoot 简单基础知识即可。1.2 环境参数开发工具:IDEA基础工具:Maven+JDK8所用技术:SpringBoot+阿里云OSS 存储服务 Java 相关APISpringBoot版本:2.1.01.3 你能学到什么SpringBoot 整合 阿里云OSS 存储服务并编写相关工具类SpringBoot 整合 thymeleaf 并实现前后端传值SpringBoot 从配置文件读取值并注入到类中如何自己搭建一个图床使用(通过前端选择图片,支持预览,但不支持修改图片)1.4 创建工程创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考下面这篇文章:https://blog.csdn.net/qq_3433…1.5 项目结构1.6 配置 pom 文件中的相关依赖 <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> <!– Thymeleaf–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!– 阿里云OSS–> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>2.4.0</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies>二 配置阿里云 OSS 存储相关属性我在项目中使用的通过常量类来配置,不过你也可以使用 .properties 配置文件来配置,然后@ConfigurationProperties 注解注入到类中。2.1 通过常量类配置(本项目使用的方式)AliyunOSSConfigConstant.java/** * @Auther: SnailClimb * @Date: 2018/12/4 15:09 * @Description: 阿里云OSS存储的相关常量配置.我这里通过常量类来配置的,当然你也可以通过.properties 配置文件来配置, * 然后利用 SpringBoot 的@ConfigurationProperties 注解来注入 /public class AliyunOSSConfigConstant { //私有构造方法 禁止该类初始化 private AliyunOSSConfigConstant() { } //仓库名称 public static final String BUCKE_NAME = “my-blog-to-use”; //地域节点 public static final String END_POINT = “oss-cn-beijing.aliyuncs.com”; //AccessKey ID public static final String AccessKey_ID = “你的AccessKeyID”; //Access Key Secret public static final String AccessKey_Secret = “你的AccessKeySecret”; //仓库中的某个文件夹 public static final String FILE_HOST = “test”;}到阿里云 OSS 控制台:https://oss.console.aliyun.com/overview获取上述相关信息:获取 BUCKE_NAME 和 END_POINT:获取AccessKey ID和Access Key Secret第一步:获取AccessKey ID和Access Key Secret第二步:2.2 通过.properties 配置#OSS配置aliyun.oss.bucketname=my-blog-to-usealiyun.oss.endpoint=oss-cn-beijing.aliyuncs.com#阿里云主账号AccessKey拥有所有API的访问权限,风险很高。建议创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。aliyun.oss.keyid=你的AccessKeyIDaliyun.oss.keysecret=你的AccessKeySecretaliyun.oss.filehost=test然后新建一个类将属性注入:@Component@PropertySource(value = “classpath:application-oss.properties”)@ConfigurationProperties(prefix = “aliyun.oss”)/* * 阿里云oss的配置类 /public class AliyunOSSConfig { private String bucketname; private String endpoint; private String keyid; private String keysecret; private String filehost; … 此处省略getter、setter以及 toString方法} 三 工具类相关方法编写该工具类主要提供了三个方法:上传文件 upLoad(File file) 、通过文件名下载文件downloadFile(String objectName, String localFileName) 、列出某个文件夹下的所有文件listFile( )。笔主比较懒,代码可能还比较简陋,各位可以懂懂自己的脑子,参考阿里云官方提供的相关文档来根据自己的需求来优化。Java API文档地址如下:https://help.aliyun.com/document_detail/32008.html?spm=a2c4g.11186623.6.703.238374b4PsMzWf/* * @Author: SnailClimb * @Date: 2018/12/1 16:56 * @Description: 阿里云OSS服务相关工具类. * Java API文档地址:https://help.aliyun.com/document_detail/32008.html?spm=a2c4g.11186623.6.703.238374b4PsMzWf /@Componentpublic class AliyunOSSUtil { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(AliyunOSSUtil.class); private static String FILE_URL; private static String bucketName = AliyunOSSConfigConstant.BUCKE_NAME; private static String endpoint = AliyunOSSConfigConstant.END_POINT; private static String accessKeyId = AliyunOSSConfigConstant.AccessKey_ID; private static String accessKeySecret = AliyunOSSConfigConstant.AccessKey_Secret; private static String fileHost = AliyunOSSConfigConstant.FILE_HOST; /* * 上传文件。 * * @param file 需要上传的文件路径 * @return 如果上传的文件是图片的话,会返回图片的"URL",如果非图片的话会返回"非图片,不可预览。文件路径为:+文件路径" / public static String upLoad(File file) { // 默认值为:true boolean isImage = true; // 判断所要上传的图片是否是图片,图片可以预览,其他文件不提供通过URL预览 try { Image image = ImageIO.read(file); isImage = image == null ? false : true; } catch (IOException e) { e.printStackTrace(); } logger.info("——OSS文件上传开始——–" + file.getName()); SimpleDateFormat format = new SimpleDateFormat(“yyyy-MM-dd”); String dateStr = format.format(new Date()); // 判断文件 if (file == null) { return null; } // 创建OSSClient实例。 OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); try { // 判断容器是否存在,不存在就创建 if (!ossClient.doesBucketExist(bucketName)) { ossClient.createBucket(bucketName); CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead); ossClient.createBucket(createBucketRequest); } // 设置文件路径和名称 String fileUrl = fileHost + “/” + (dateStr + “/” + UUID.randomUUID().toString().replace("-", “”) + “-” + file.getName()); if (isImage) {//如果是图片,则图片的URL为:…. FILE_URL = “https://” + bucketName + “.” + endpoint + “/” + fileUrl; } else { FILE_URL = “非图片,不可预览。文件路径为:” + fileUrl; } // 上传文件 PutObjectResult result = ossClient.putObject(new PutObjectRequest(bucketName, fileUrl, file)); // 设置权限(公开读) ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead); if (result != null) { logger.info("——OSS文件上传成功——" + fileUrl); } } catch (OSSException oe) { logger.error(oe.getMessage()); } catch (ClientException ce) { logger.error(ce.getErrorMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } return FILE_URL; } /* * 通过文件名下载文件 * * @param objectName 要下载的文件名 * @param localFileName 本地要创建的文件名 / public static void downloadFile(String objectName, String localFileName) { // 创建OSSClient实例。 OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); // 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖,不存在则新建。 ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File(localFileName)); // 关闭OSSClient。 ossClient.shutdown(); } /* * 列举 test 文件下所有的文件 / public static void listFile() { // 创建OSSClient实例。 OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); // 构造ListObjectsRequest请求。 ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName); // 设置prefix参数来获取fun目录下的所有文件。 listObjectsRequest.setPrefix(“test/”); // 列出文件。 ObjectListing listing = ossClient.listObjects(listObjectsRequest); // 遍历所有文件。 System.out.println(“Objects:”); for (OSSObjectSummary objectSummary : listing.getObjectSummaries()) { System.out.println(objectSummary.getKey()); } // 遍历所有commonPrefix。 System.out.println(“CommonPrefixes:”); for (String commonPrefix : listing.getCommonPrefixes()) { System.out.println(commonPrefix); } // 关闭OSSClient。 ossClient.shutdown(); }}四 Controller 层编写相关测试方法上传文件 upLoad(File file) 、通过文件名下载文件downloadFile(String objectName, String localFileName) 、列出某个文件夹下的所有文件listFile( ) 这三个方法都在下面有对应的简单测试。另外,还有一个方法 uploadPicture(@RequestParam(“file”) MultipartFile file, Model model)对应于我们等下要实现的图床功能,该方法从前端接受到图片之后上传到阿里云OSS存储空间并返回上传成功的图片 URL 地址给前端。注意将下面的相关路径改成自己的,不然会报错!!!/* * @Author: SnailClimb * @Date: 2018/12/2 16:56 * @Description: 阿里云OSS服务Controller /@Controller@RequestMapping("/oss")public class AliyunOSSController { private final org.slf4j.Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private AliyunOSSUtil aliyunOSSUtil; /* * 测试上传文件到阿里云OSS存储 * * @return / @RequestMapping("/testUpload") @ResponseBody public String testUpload() { File file = new File(“E:/Picture/test.jpg”); AliyunOSSUtil aliyunOSSUtil = new AliyunOSSUtil(); String url = aliyunOSSUtil.upLoad(file); System.out.println(url); return “success”; } /* * 通过文件名下载文件 / @RequestMapping("/testDownload") @ResponseBody public String testDownload() { AliyunOSSUtil aliyunOSSUtil = new AliyunOSSUtil(); aliyunOSSUtil.downloadFile( “test/2018-12-04/e3f892c27f07462a864a43b8187d4562-rawpixel-600782-unsplash.jpg”,“E:/Picture/e3f892c27f07462a864a43b8187d4562-rawpixel-600782-unsplash.jpg”); return “success”; } /* * 列出某个文件夹下的所有文件 / @RequestMapping("/testListFile") @ResponseBody public String testListFile() { AliyunOSSUtil aliyunOSSUtil = new AliyunOSSUtil(); aliyunOSSUtil.listFile(); return “success”; } /* * 文件上传(供前端调用) / @RequestMapping(value = “/uploadFile”) public String uploadPicture(@RequestParam(“file”) MultipartFile file, Model model) { logger.info(“文件上传”); String filename = file.getOriginalFilename(); System.out.println(filename); try { if (file != null) { if (!"".equals(filename.trim())) { File newFile = new File(filename); FileOutputStream os = new FileOutputStream(newFile); os.write(file.getBytes()); os.close(); file.transferTo(newFile); // 上传到OSS String uploadUrl = aliyunOSSUtil.upLoad(newFile); model.addAttribute(“url”,uploadUrl); } } } catch (Exception ex) { ex.printStackTrace(); } return “success”; }}五 启动类@SpringBootApplicationpublic class SpringbootOssApplication { public static void main(String[] args) { SpringApplication.run(SpringbootOssApplication.class, args); }}六 上传图片相关前端页面注意引入jquery ,避免前端出错。index.htmlJS 的内容主要是让我们上传的图片可以预览,就像我们在网站更换头像的时候一样。<!DOCTYPE html><html xmlns=“http://www.w3.org/1999/xhtml" xmlns:th=“http://www.thymeleaf.org”><head> <meta charset=“UTF-8”> <title>基于阿里云OSS存储的图床</title> <script th:src=”@{/js/jquery-3.3.1.js}"></script> <style> * { margin: 0; padding: 0; } #submit { margin-left: 15px; } .preview_box img { width: 200px; } </style></head><body><form action="/oss/uploadFile" enctype=“multipart/form-data” method=“post”> <div class=“form-group” id=“group”> <input type=“file” id=“img_input” name=“file” accept=“image/"> <label for=“img_input” ></label> </div> <button type=“submit” id=“submit”>上传</button> <!–预览图片–> <div class=“preview_box”></div></form><script type=“text/javascript”> $("#img_input”).on(“change”, function (e) { var file = e.target.files[0]; //获取图片资源 // 只选择图片文件 if (!file.type.match(‘image.*’)) { return false; } var reader = new FileReader(); reader.readAsDataURL(file); // 读取文件 // 渲染文件 reader.onload = function (arg) { var img = ‘<img class=“preview” src="’ + arg.target.result + ‘" alt=“preview”/>’; $(".preview_box").empty().append(img); } });</script></body></html>success.html通过 <span th:text="${url}"></span> 引用后端传过来的值。<!DOCTYPE html><html xmlns=“http://www.w3.org/1999/xhtml" xmlns:th=“http://www.thymeleaf.org”><head> <meta charset=“UTF-8”> <title>上传结果</title></head><body><h1>上传成功!</h1>图片地址为:<span th:text="${url}"></span></body></html>七 测试我们的图床访问 :http://localhost:8080/① 上传图片② 图片上传成功返回图片地址③ 通过图片 URL 访问图片我们终于能够独立利用阿里云 OSS 完成一个自己的图床服务,但是其实如果你想用阿里云OSS当做图床可以直接使用极简图床:http://jiantuku.com 上传图片,比较方便!大家可能心里在想那你特么让我实现个图床干嘛?我觉得通过学习,大家以后可以做很多事情,比如 利用阿里云OSS 存储服务存放自己网站的相关图片。ThoughtWorks准入职Java工程师。专注Java知识分享!开源 Java 学习指南——JavaGuide(12k+ Star)的作者。公众号多篇文章被各大技术社区转载。公众号后台回复关键字“1”可以领取一份我精选的Java资源哦! ...

December 6, 2018 · 5 min · jiezi

新手也能实现,基于SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置

在上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》中,带着大家整合了 SpringBoot 和 Mybatis ,我们在当时使用的时单数据源的情况,这种情况下 Spring Boot的配置非常简单,只需要在 application.properties 文件中配置数据库的相关连接参数即可。但是往往随着业务量发展,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源。下面基于 SpringBoot+Mybatis ,带着大家看一下 SpringBoot 中如何配置多数据源。这篇文章所涉及的代码其实是基于上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》 的项目写的,但是为了考虑部分读者没有读过上一篇文章,所以我还是会一步一步带大家走完每一步,力争新手也能在看完之后独立实践。目录:<!– MarkdownTOC –>一 开发前的准备1.1 环境参数1.2 创建工程1.3 创建两个数据库和 user 用户表、money工资详情表1.4 配置 pom 文件中的相关依赖1.5 配置 application.properties1.6 创建用户类 Bean和工资详情类 Bean二 数据源配置三 Dao 层开发和 Service 层开发3.1 Dao 层3.2 Service 层四 Controller层五 启动类<!– /MarkdownTOC –>一 开发前的准备1.1 环境参数开发工具:IDEA基础工具:Maven+JDK8所用技术:SpringBoot+Mybatis数据库:MySQLSpringBoot版本:2.1.0. SpringBoot2.0之后会有一些小坑,这篇文章会给你介绍到。注意版本不一致导致的一些小问题。1.2 创建工程创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考下面这篇文章:https://blog.csdn.net/qq_34337272/article/details/79563606本项目结构:1.3 创建两个数据库和 user 用户表、money工资详情表我们一共创建的两个数据库,然后分别在这两个数据库中创建了 user 用户表、money工资详情表。我们的用户表很简单,只有 4 个字段:用户 id、姓名、年龄、余额。如下图所示:添加了“余额money”字段是为了给大家简单的演示一下事务管理的方式。我们的工资详情表也很简单,也只有 4 个字段: id、基本工资、奖金和惩罚金。如下图所示:建表语句:用户表:CREATE TABLE user ( id int(13) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, name varchar(33) DEFAULT NULL COMMENT ‘姓名’, age int(3) DEFAULT NULL COMMENT ‘年龄’, money double DEFAULT NULL COMMENT ‘账户余额’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8工资详情表:CREATE TABLE money ( id int(33) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, basic int(33) DEFAULT NULL COMMENT ‘基本工资’, reward int(33) DEFAULT NULL COMMENT ‘奖金’, punishment int(33) DEFAULT NULL COMMENT ‘惩罚金’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf81.4 配置 pom 文件中的相关依赖由于要整合 springboot 和 mybatis 所以加入了artifactId 为 mybatis-spring-boot-starter 的依赖,由于使用了Mysql 数据库 所以加入了artifactId 为 mysql-connector-java 的依赖。 <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>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>1.5 配置 application.properties配置两个数据源:数据库1和数据库2!注意事项:在1.0 配置数据源的过程中主要是写成:spring.datasource.url 和spring.datasource.driverClassName。而在2.0升级之后需要变更成:spring.datasource.jdbc-url和spring.datasource.driver-class-name!不然在连接数据库时可能会报下面的错误:### Error querying database. Cause: java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.另外在在2.0.2+版本后需要在datasource后面加上hikari,如果你没有加的话,同样可能会报错。server.port=8335# 配置第一个数据源spring.datasource.hikari.db1.jdbc-url=jdbc:mysql://127.0.0.1:3306/erp?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8spring.datasource.hikari.db1.username=rootspring.datasource.hikari.db1.password=153963spring.datasource.hikari.db1.driver-class-name=com.mysql.cj.jdbc.Driver# 配置第二个数据源spring.datasource.hikari.db2.jdbc-url=jdbc:mysql://127.0.0.1:3306/erp2?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8spring.datasource.hikari.db2.username=rootspring.datasource.hikari.db2.password=153963spring.datasource.hikari.db2.driver-class-name=com.mysql.cj.jdbc.Driver1.6 创建用户类 Bean和工资详情类 BeanUser.javapublic class User { private int id; private String name; private int age; private double money; … 此处省略getter、setter以及 toString方法}Money.javapublic class Money { private int basic; private int reward; private int punishment; … 此处省略getter、setter以及 toString方法}二 数据源配置通过 Java 类来实现对两个数据源的配置,这一部分是最关键的部分了,这里主要提一下下面这几点:@MapperScan 注解中我们声明了使用数据库1的dao类所在的位置,还声明了 SqlSessionTemplate 。SqlSessionTemplate是MyBatis-Spring的核心。这个类负责管理MyBatis的SqlSession,调用MyBatis的SQL方法,翻译异常。SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用。由于我使用的是全注解的方式开发,所以下面这条找并且解析 mapper.xml 配置语句被我注释掉了bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:mybatis/mapper/db2/*.xml”));比如我们要声明使用数据1,直接在 dao 层的类上加上这样一个注释即可:@Qualifier(“db1SqlSessionTemplate”)我们在数据库1配置类的每个方法前加上了 @Primary 注解来声明这个数据库时默认数据库,不然可能会报错。DataSource1Config.java@Configuration@MapperScan(basePackages = “top.snailclimb.db1.dao”, sqlSessionTemplateRef = “db1SqlSessionTemplate”)public class DataSource1Config { /** * 生成数据源. @Primary 注解声明为默认数据源 / @Bean(name = “db1DataSource”) @ConfigurationProperties(prefix = “spring.datasource.hikari.db1”) @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } /* * 创建 SqlSessionFactory / @Bean(name = “db1SqlSessionFactory”) @Primary public SqlSessionFactory testSqlSessionFactory(@Qualifier(“db1DataSource”) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); // bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:mybatis/mapper/db1/.xml”)); return bean.getObject(); } /** * 配置事务管理 / @Bean(name = “db1TransactionManager”) @Primary public DataSourceTransactionManager testTransactionManager(@Qualifier(“db1DataSource”) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = “db1SqlSessionTemplate”) @Primary public SqlSessionTemplate testSqlSessionTemplate(@Qualifier(“db1SqlSessionFactory”) SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }}DataSource2Config.java@Configuration@MapperScan(basePackages = “top.snailclimb.db2.dao”, sqlSessionTemplateRef = “db2SqlSessionTemplate”)public class DataSource2Config { @Bean(name = “db2DataSource”) @ConfigurationProperties(prefix = “spring.datasource.hikari.db2”) public DataSource testDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = “db2SqlSessionFactory”) public SqlSessionFactory testSqlSessionFactory(@Qualifier(“db2DataSource”) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:mybatis/mapper/db2/.xml”)); return bean.getObject(); } @Bean(name = “db2TransactionManager”) public DataSourceTransactionManager testTransactionManager(@Qualifier(“db2DataSource”) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = “db2SqlSessionTemplate”) public SqlSessionTemplate testSqlSessionTemplate(@Qualifier(“db2SqlSessionFactory”) SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }}三 Dao 层开发和 Service 层开发新建两个不同的包存放两个不同数据库的 dao 和 service。3.1 Dao 层对于两个数据库,我们只是简单的测试一个查询这个操作。在上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》中,我带着大家使用注解实现了数据库基本的增删改查操作。UserDao.java@Qualifier(“db1SqlSessionTemplate”)public interface UserDao { /** * 通过名字查询用户信息 / @Select(“SELECT * FROM user WHERE name = #{name}”) User findUserByName(String name);}MoneyDao.java@Qualifier(“db2SqlSessionTemplate”)public interface MoneyDao { /* * 通过id 查看工资详情 / @Select(“SELECT * FROM money WHERE id = #{id}”) Money findMoneyById(@Param(“id”) int id);}3.2 Service 层Service 层很简单,没有复杂的业务逻辑。UserService.java@Servicepublic class UserService { @Autowired private UserDao userDao; /* * 根据名字查找用户 / public User selectUserByName(String name) { return userDao.findUserByName(name); }}MoneyService.java@Servicepublic class MoneyService { @Autowired private MoneyDao moneyDao; /* * 根据名字查找用户 */ public Money selectMoneyById(int id) { return moneyDao.findMoneyById(id); }}四 Controller层Controller 层也非常简单。UserController.java@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @RequestMapping("/query") public User testQuery() { return userService.selectUserByName(“Daisy”); }}MoneyController.java@RestController@RequestMapping("/money")public class MoneyController { @Autowired private MoneyService moneyService; @RequestMapping("/query") public Money testQuery() { return moneyService.selectMoneyById(1); }}五 启动类//此注解表示SpringBoot启动类@SpringBootApplicationpublic class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); }}这样基于SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置就已经完成了, 两个数据库都可以被访问了。 ...

December 3, 2018 · 3 min · jiezi

基于 SpringBoot2.0+优雅整合 SpringBoot+Mybatis

Github 地址:https://github.com/Snailclimb/springboot-integration-examples(SpringBoot和其他常用技术的整合,可能是你遇到的讲解最详细的学习案例,力争新手也能看懂并且能够在看完之后独立实践。基于最新的 SpringBoot2.0+,是你学习SpringBoot 的最佳指南。) ,欢迎各位 Star。SpringBoot 整合 Mybatis 有两种常用的方式,一种就是我们常见的 xml 的方式 ,还有一种是全注解的方式。我觉得这两者没有谁比谁好,在 SQL 语句不太长的情况下,我觉得全注解的方式一定是比较清晰简洁的。但是,复杂的 SQL 确实不太适合和代码写在一起。下面就开始吧!目录:一 开发前的准备1.1 环境参数1.2 创建工程1.3 创建数据库和 user 用户表1.4 配置 pom 文件中的相关依赖1.5 配置 application.properties1.6 创建用户类 Bean二 全注解的方式2.1 Dao 层开发2.2 service 层2.3 Controller 层2.4 启动类2.5 简单测试三 xml 的方式3.1 Dao 层的改动3.2 配置文件的改动一 开发前的准备1.1 环境参数开发工具:IDEA基础工具:Maven+JDK8所用技术:SpringBoot+Mybatis数据库:MySQLSpringBoot版本:2.1.01.2 创建工程创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考下面这篇文章:https://blog.csdn.net/qq_34337272/article/details/795636061.3 创建数据库和 user 用户表我们的数据库很简单,只有 4 个字段:用户 id、姓名、年龄、余额,如下图所示:添加了“余额money”字段是为了给大家简单的演示一下事务管理的方式。建表语句:CREATE TABLE user ( id int(13) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, name varchar(33) DEFAULT NULL COMMENT ‘姓名’, age int(3) DEFAULT NULL COMMENT ‘年龄’, money double DEFAULT NULL COMMENT ‘账户余额’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf81.4 配置 pom 文件中的相关依赖由于要整合 springboot 和 mybatis 所以加入了artifactId 为 mybatis-spring-boot-starter 的依赖,由于使用了Mysql 数据库 所以加入了artifactId 为 mysql-connector-java 的依赖。 <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>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>1.5 配置 application.properties由于我使用的是比较新的Mysql连接驱动,所以配置文件可能和之前有一点不同。server.port=8333spring.datasource.url=jdbc:mysql://127.0.0.1:3306/erp?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8spring.datasource.username=rootspring.datasource.password=153963spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver注意:我们使用的 mysql-connector-java 8+ ,JDBC 连接到mysql-connector-java 6+以上的需要指定时区 serverTimezone=GMT%2B8。另外我们之前使用配置 Mysql数据连接是一般是这样指定driver-class-name=com.mysql.jdbc.Driver,但是现在不可以必须为 否则控制台下面的异常:Loading class com.mysql.jdbc.Driver'. This is deprecated. The new driver class is com.mysql.cj.jdbc.Driver’. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.上面异常的意思是:com.mysql.jdbc.Driver 被弃用了。新的驱动类是 com.mysql.cj.jdbc.Driver。驱动程序通过SPI自动注册,手动加载类通常是不必要。如果你非要写把com.mysql.jdbc.Driver 改为com.mysql.cj.jdbc.Driver 即可。1.6 创建用户类 Beanpublic class User { private int id; private String name; private int age; private double money; … 此处省略getter、setter以及 toString方法}二 全注解的方式先来看一下 全注解的方式,这种方式和后面提到的 xml 的方式的区别仅仅在于 一个将 sql 语句写在 java 代码中,一个写在 xml 配置文件中。全注方式解转换成 xml 方式仅需做一点点改变即可,我在后面会提到。项目结构:2.1 Dao 层开发UserDao.java@Mapperpublic interface UserDao { /** * 通过名字查询用户信息 / @Select(“SELECT * FROM user WHERE name = #{name}”) User findUserByName(@Param(“name”) String name); /* * 查询所有用户信息 / @Select(“SELECT * FROM user”) List<User> findAllUser(); /* * 插入用户信息 / @Insert(“INSERT INTO user(name, age,money) VALUES(#{name}, #{age}, #{money})”) void insertUser(@Param(“name”) String name, @Param(“age”) Integer age, @Param(“money”) Double money); /* * 根据 id 更新用户信息 / @Update(“UPDATE user SET name = #{name},age = #{age},money= #{money} WHERE id = #{id}”) void updateUser(@Param(“name”) String name, @Param(“age”) Integer age, @Param(“money”) Double money, @Param(“id”) int id); /* * 根据 id 删除用户信息 / @Delete(“DELETE from user WHERE id = #{id}”) void deleteUser(@Param(“id”) int id);}2.2 service 层@Servicepublic class UserService { @Autowired private UserDao userDao; /* * 根据名字查找用户 / public User selectUserByName(String name) { return userDao.findUserByName(name); } /* * 查找所有用户 / public List<User> selectAllUser() { return userDao.findAllUser(); } /* * 插入两个用户 / public void insertService() { userDao.insertUser(“SnailClimb”, 22, 3000.0); userDao.insertUser(“Daisy”, 19, 3000.0); } /* * 根据id 删除用户 / public void deleteService(int id) { userDao.deleteUser(id); } /* * 模拟事务。由于加上了 @Transactional注解,如果转账中途出了意外 SnailClimb 和 Daisy 的钱都不会改变。 / @Transactional public void changemoney() { userDao.updateUser(“SnailClimb”, 22, 2000.0, 3); // 模拟转账过程中可能遇到的意外状况 int temp = 1 / 0; userDao.updateUser(“Daisy”, 19, 4000.0, 4); }}2.3 Controller 层@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @RequestMapping("/query") public User testQuery() { return userService.selectUserByName(“Daisy”); } @RequestMapping("/insert") public List<User> testInsert() { userService.insertService(); return userService.selectAllUser(); } @RequestMapping("/changemoney") public List<User> testchangemoney() { userService.changemoney(); return userService.selectAllUser(); } @RequestMapping("/delete") public String testDelete() { userService.deleteService(3); return “OK”; }}2.4 启动类//此注解表示SpringBoot启动类@SpringBootApplication// 此注解表示动态扫描DAO接口所在包,实际上不加下面这条语句也可以找到@MapperScan(“top.snailclimb.dao”)public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); }}2.5 简单测试上述代码经过测试都没问题,这里贴一下根据姓名查询的测试的结果。三 xml 的方式项目结构:相比于注解的方式主要有以下几点改变,非常容易实现。3.1 Dao 层的改动我这里只演示一个根据姓名找人的方法。UserDao.java@Mapperpublic interface UserDao { /* * 通过名字查询用户信息 / User findUserByName(String name);}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=“top.snailclimb.dao.UserDao”> <select id=“findUserByName” parameterType=“String” resultType=“top.snailclimb.bean.User”> SELECT * FROM user WHERE name = #{name} </select></mapper>3.2 配置文件的改动配置文件中加入下面这句话:mybatis.mapper-locations=classpath:mapper/.xmlThoughtWorks准入职Java工程师。专注Java知识分享!开源 Java 学习指南——JavaGuide(12k+ Star)的作者。公众号多篇文章被各大技术社区转载。公众号后台回复关键字“1”可以领取一份我精选的Java资源哦! ...

November 30, 2018 · 3 min · jiezi

Spring Flux中Request与HandlerMapping关系的形成过程

一、前言Spring Flux中的核心DispatcherHandler的处理过程分为三步,其中首步就是通过HandlerMapping接口查找Request所对应的Handler。本文就是通过阅读源码的方式,分析一下HandlerMapping接口的实现者之一——RequestMappingHandlerMapping类,用于处理基于注解的路由策略,把所有用@Controller和@RequestMapping标记的类中的Handler识别出来,以便DispatcherHandler调用的。HandlerMapping接口的另外两种实现类:1、RouterFunctionMapping用于函数式端点的路由;2、SimpleUrlHandlerMapping用于显式注册的URL模式与WebHandler配对。<!– more –>文章系列关于非阻塞IO:《从时间碎片角度理解阻塞IO模型及非阻塞模型》关于SpringFlux新手入门:《快速上手Spring Flux框架》为什么Spring要引入SpringFlux框架 尚未完成Spring Flux中Request与HandlerMapping关系的形成过程 本文Spring Flux中执行HandlerMapping的过程 尚未完成Spring Flux中是如何处理HandlerResult的 尚未完成Spring Flux与WEB服务器之Servlet 3.1+ 尚未完成Spring Flux与WEB服务器之Netty 尚未完成二、对基于注解的路由控制器的抽象Spring中基于注解的控制器的使用方法大致如下:@Controllerpublic class MyHandler{ @RequestMapping("/") public String handlerMethod(){ }}在Spring WebFlux中,对上述使用方式进行了三层抽象模型。Mapping用户定义的基于annotation的映射关系该抽象对应的类是:org.springframework.web.reactive.result.method.RequestMappingInfo比如上述例子中的 @RequestMapping("/")所代表的映射关系Handler代表控制器的类该抽象对应的类是:java.lang.Class比如上述例子中的MyHandler类Method具体处理映射的方法该抽象对应的类是:java.lang.reflect.Method比如上述例子中的String handlerMethod()方法基于上述三层抽象模型,进而可以作一些组合。HandlerMethodHandler与Method的结合体,Handler(类)与Method(方法)搭配后就成为一个可执行的单元了Mapping vs HandlerMethod把Mapping与HandlerMethod作为字典存起来,就可以根据请求中的关键信息(路径、头信息等)来匹配到Mapping,再根据Mapping找到HandlerMethod,然后执行HandlerMethod,并传递随请求而来的参数。理解了这个抽象模型后,接下来分析源码来理解Spring WebFlux如何处理请求与Handler之间的Mapping关系时,就非常容易了。HandlerMapping接口及其各实现类负责上述模型的构建与运作。三、HandlerMapping接口实现的设计模式HandlerMapping接口实现,采用了"模版方法"这种设计模式。1层:AbstractHandlerMapping implements HandlerMapping, Ordered, BeanNameAware ^ | 2层:AbstractHandlerMethodMapping implements InitializingBean ^ | 3层:RequestMappingInfoHandlerMapping ^ | 4层:RequestMappingHandlerMapping implements EmbeddedValueResolverAware 下面对各层的职责作简要说明:第1层主要实现了对外提供模型的接口即重载了HandlerMapping接口的"Mono<Object> getHandler(ServerWebExchange exchange) “方法,并定义了骨架代码。第2层有两个责任 —— 解析用户定义的HandlerMethod + 实现对外提供模型接口实现所需的抽象方法通过实现了InitializingBean接口的"void afterPropertiesSet()“方法,解析用户定义的Handler和Method。实现第1层对外提供模型接口实现所需的抽象方法:“Mono<?> getHandlerInternal(ServerWebExchange exchange)“第3层提供根据请求匹配Mapping模型实例的方法第4层实现一些高层次用到的抽象方法来创建具体的模型实例。小结一下,就是HandlerMapping接口及其实现类,把用户定义的各Controller等,抽象为上述的Mapping、Handler及Method模型,并将Mapping与HandlerMethod作为字典关系存起来,还提供通过匹配请求来获得HandlerMethod的公共方法。接下来的章节,将先分析解析用户定义的模型并缓存模型的过程,然后再分析一下匹配请求来获得HandlerMethod的公共方法的过程。四、解析用户定义的模型并缓存模型的过程4-1、实现InitializingBean接口第2层AbstractHandlerMethodMapping抽象类中的一个重要方法——实现了InitializingBean接口的"void afterPropertiesSet()“方法,为Spring WebFlux带来了解析用户定义的模型并缓存模型的机会 —— Spring容器初初始化完成该类的具体类的Bean后,将会回调这个方法。在该方法中,实现获取用户定义的Handler、Method、Mapping以及缓存Mapping与HandlerMethod映射关系的功能。@Overridepublic void afterPropertiesSet() { initHandlerMethods(); // Total includes detected mappings + explicit registrations via registerMapping.. …}4-2、找到用户定义的HandlerafterPropertiesSet方法中主要是调用了void initHandlerMethods()方法,具体如下:protected void initHandlerMethods() { //获取Spring容器中所有Bean名字 String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { //获取Bean的类型 beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { // An unresolvable bean type, probably from a lazy bean - let’s ignore it. if (logger.isTraceEnabled()) { logger.trace(“Could not resolve type for bean ‘” + beanName + “’”, ex); } } //如果获取到类型,并且类型是Handler,则继续加载Handler方法。 if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } } //初始化后收尾工作 handlerMethodsInitialized(getHandlerMethods());}这儿首先获取Spring容器中所有Bean名字,然后循环处理每一个Bean。如果Bean名称不是以SCOPED_TARGET_NAME_PREFIX常量开头,则获取Bean的类型。如果获取到类型,并且类型是Handler,则继续加载Handler方法。isHandler(beanType)调用,检查Bean的类型是否符合handler定义。AbstractHandlerMethodMapping抽象类中定义的抽象方法"boolean isHandler(Class<?> beanType)",是由RequestMappingHandlerMapping类实现的。具体实现代码如下:protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));}不难看出,对于RequestMappingHandlerMapping这个实现类来说,只有拥有@Controller或者@RequestMapping注解的类,才是Handler。(言下之意对于其他实现类来说Handler的定义不同)。具体handler的定义,在HandlerMapping各实现类来说是不同的,这也是isHandler抽象方法由具体实现类来实现的原因。4-3、发现Handler的Method接下来我们要重点看一下"detectHandlerMethods(beanName);“这个方法调用。protected void detectHandlerMethods(final Object handler) { Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { //将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型) final Class<?> userType = ClassUtils.getUserClass(handlerType); //寻找目标类型userType中的Methods,selectMethods方法的第二个参数是lambda表达式,即感兴趣的方法的过滤规则 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, //回调函数metadatalookup将通过controller定义的mapping与手动定义的mapping合并起来 (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); if (logger.isTraceEnabled()) { logger.trace(“Mapped " + methods.size() + " handler method(s) for " + userType + “: " + methods); } methods.forEach((key, mapping) -> { //再次核查方法与类型是否匹配 Method invocableMethod = AopUtils.selectInvocableMethod(key, userType); //如果是满足要求的方法,则注册到全局的MappingRegistry实例里 registerHandlerMethod(handler, invocableMethod, mapping); }); }}首先将参数handler(即外部传入的BeanName或者BeanType)转换为Class<?>类型变量handlerType。如果转换成功,再将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型)。接下来获取该用户类型里所有的方法(Method)。循环处理每个方法,如果是满足要求的方法,则注册到全局的MappingRegistry实例里。4-4、解析Mapping信息其中,以下代码片段有必要深入探究一下 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));MethodIntrospector.selectMethods方法的调用,将会把用@RequestMapping标记的方法筛选出来,并交给第二个参数所定义的MetadataLookup回调函数将通过controller定义的mapping与手动定义的mapping合并起来。第二个参数是用lambda表达式传入的,表达式中将method、userType传给getMappingForMethod(method, userType)方法。getMappingForMethod方法在高层次中是抽象方法,具体的是现在第4层RequestMappingHandlerMapping类中实现。在具体实现getMappingForMethod时,会调用到RequestMappingHandlerMapping类的下面这个方法。从该方法中,我们可以看到,首先会获得参数element(即用户在Controller中定义的方法)的RequestMapping类型的类实例,然后构造代表Mapping抽象模型的RequestmappingInfo类型实例并返回。private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); }构造代表Mapping抽象模型的RequestmappingInfo类型实例,用的是createRequestMappingInfo方法,如下。可以看到RequestMappingInfo所需要的信息,包括paths、methods、params、headers、consumers、produces、mappingName,即用户定义@RequestMapping注解时所设定的可能的参数,都被存在这儿了。拥有了这些信息,当请求来到时,RequestMappingInfo就可以测试自身是否是处理该请求的人选之一了。protected RequestMappingInfo createRequestMappingInfo( RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) { RequestMappingInfo.Builder builder = RequestMappingInfo .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()); if (customCondition != null) { builder.customCondition(customCondition); } return builder.options(this.config).build(); }4-5、缓存Mapping与HandlerMethod关系最后,registerHandlerMethod(handler, invocableMethod, mapping)调用将缓存HandlerMethod,其中mapping参数是RequestMappingInfo类型的。。内部调用的是MappingRegistry实例的void register(T mapping, Object handler, Method method)方法,其中T是RequestMappingInfo类型。MappingRegistry类维护所有指向Handler Methods的映射,并暴露方法用于查找映射,同时提供并发控制。public void register(T mapping, Object handler, Method method) { this.readWriteLock.writeLock().lock(); try { HandlerMethod handlerMethod = createHandlerMethod(handler, method); …… this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); } finally { this.readWriteLock.writeLock().unlock(); } }五、匹配请求来获得HandlerMethodAbstractHandlerMethodMapping类的“Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange)”方法,具体实现了根据请求查找HandlerMethod的逻辑。 @Override public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) { //获取读锁 this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod; try { //调用其它方法继续查找HandlerMethod handlerMethod = lookupHandlerMethod(exchange); } catch (Exception ex) { return Mono.error(ex); } if (handlerMethod != null) { handlerMethod = handlerMethod.createWithResolvedBean(); } return Mono.justOrEmpty(handlerMethod); } //释放读锁 finally { this.mappingRegistry.releaseReadLock(); } }handlerMethod = lookupHandlerMethod(exchange)调用,继续查找HandlerMethod。我们继续看一下HandlerMethod lookupHandlerMethod(ServerWebExchange exchange)方法的定义。为方便阅读,我把注释也写在了代码里。 protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception { List<Match> matches = new ArrayList<>(); //查找所有满足请求的Mapping,并放入列表mathes addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); if (!matches.isEmpty()) { //获取比较器comparator Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange)); //使用比较器将列表matches排序 matches.sort(comparator); //将排在第1位的作为最佳匹配项 Match bestMatch = matches.get(0); if (matches.size() > 1) { //将排在第2位的作为次佳匹配项 Match secondBestMatch = matches.get(1); } handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange); return bestMatch.handlerMethod; } else { return handleNoMatch(this.mappingRegistry.getMappings().keySet(), exchange); } }六、总结理解了Spring WebFlux在获取映射关系方面的抽象设计模型后,就很容易读懂代码,进而更加理解框架的具体处理方式,在使用框架时做到“知己知彼”。原文:http://www.yesdata.net/2018/11/27/spring-flux-request-mapping/ ...

November 30, 2018 · 3 min · jiezi

利用springboot创建多模块项目

本文旨在用最通俗的语言讲述最枯燥的基本知识最近要对一个不大不小的项目进行重构,用spring觉得太过于繁琐,用cloud又有觉得过于庞大,维护的人手不够;权衡之下,最终选了springboot作为架子,但是因为项目涉及的业务模块较多,各个模块之间的业务交流不是很多,相对独立,因此想着把项目做成多模块的形式,模块之间可以独立部署,又可以互相调用,满足需求,故而花了点时间,搭了个springboot多模块的架子。文章提纲:多模块的创建关键配置温馨提示1. 根模块的创建springboot的多模块项目构建主要有以下步骤:父模块的创建和设置:打开idea-》选择Create New Project-》spring initialize-》填写项目名称-》next-》next-》完成父模块的创建。打开父模块的pom。把package的值改为pom。子模块的创建和设置:在创建好的父模块中右键-》New-》module-》spring initialize-》填写项目名称-》选择项目中需要的部件-》next-》完成父模块的创建。按照步骤1,创建其它模块在父模块的pom中,增加modules节点,把所有子模块加入到父模块中。 <!–引入多模块–> <modules> <module>module-one</module> <module>module-two</module> </modules>模块间的互相调用在需要调用其它模块的模块的pom文件中,增加对其它模块的依赖即可。<dependency> <groupId>com.example</groupId> <artifactId>module-one</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>由于项目构建都是用idea完成的,一个个截图的方式可能有些看管不能看清楚,因此在此选择用视频的方式,具体过程请看下方视频:点我查看视频教程:《利用springboot创建多模块项目》2. 关键配置看完视频之后,作者会发现,构建一个springboot多模块项目真的太简单了,只需要做好几个关键地方的配置就可以了.父模块的src,直接删掉父模块的pom文件中,打包方式改成pom.子模块的创建要在父模块下以module的形式创建子模块创建成功之后,在父模块中增加子模块的module模块之间的相关关系,用依赖来表示。3. 温馨提示文章仅讲述springboot创建多模块,搭建一个多模块架子,并未对其它组件进行集成,有需要的读者根据自己的需求,在创建模块的时候,选择需要的组件即可。对于多个模块共同的依赖,在父pom中设置即可。对于多模块项目的打包发布,当需要构建某个模块发布时,选择父pom构建,install -pl open-api -am觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能

November 16, 2018 · 1 min · jiezi