Spring Boot(十四)RabbitMQ延迟队列

一、前言延迟队列的使用场景:1.未按时支付的订单,30分钟过期之后取消订单;2.给活跃度比较低的用户间隔N天之后推送消息,提高活跃度;3.过1分钟给新注册会员的用户,发送注册邮件等。<!–more–>实现延迟队列的方式有两种:通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能;使用rabbitmq-delayed-message-exchange插件实现延迟功能;注意: 延迟插件rabbitmq-delayed-message-exchange是在RabbitMQ 3.5.7及以上的版本才支持的,依赖Erlang/OPT 18.0及以上运行环境。由于使用死信交换器相对曲折,本文重点介绍第二种方式,使用rabbitmq-delayed-message-exchange插件完成延迟队列的功能。二、安装延迟插件1.1 下载插件打开官网下载:http://www.rabbitmq.com/commu…选择相应的对应的版本“3.7.x”点击下载。注意: 下载的是.zip的安装包,下载完之后需要手动解压。1.2 安装插件拷贝插件到Docker:docker cp D:\rabbitmq_delayed_message_exchange-20171201-3.7.x.ez rabbit:/pluginsRabbitMQ在Docker的安装,请参照本系列的上一篇文章:http://www.apigo.cn/2018/09/1…1.3 启动插件进入docker内部:docker exec -it rabbit /bin/bash开启插件:rabbitmq-plugins enable rabbitmq_delayed_message_exchange查询安装的所有插件:rabbitmq-plugins list安装正常,效果如下图:重启RabbitMQ,使插件生效docker restart rabbit三、代码实现3.1 配置队列import com.example.rabbitmq.mq.DirectConfig;import org.springframework.amqp.core.*;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.HashMap;import java.util.Map;@Configurationpublic class DelayedConfig { final static String QUEUE_NAME = “delayed.goods.order”; final static String EXCHANGE_NAME = “delayedec”; @Bean public Queue queue() { return new Queue(DelayedConfig.QUEUE_NAME); } // 配置默认的交换机 @Bean CustomExchange customExchange() { Map<String, Object> args = new HashMap<>(); args.put(“x-delayed-type”, “direct”); //参数二为类型:必须是x-delayed-message return new CustomExchange(DelayedConfig.EXCHANGE_NAME, “x-delayed-message”, true, false, args); } // 绑定队列到交换器 @Bean Binding binding(Queue queue, CustomExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(DelayedConfig.QUEUE_NAME).noargs(); }}3.2 发送消息import org.springframework.amqp.AmqpException;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.amqp.core.Message;import org.springframework.amqp.core.MessagePostProcessor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.text.SimpleDateFormat;import java.util.Date;@Componentpublic class DelayedSender { @Autowired private AmqpTemplate rabbitTemplate; public void send(String msg) { SimpleDateFormat sf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); System.out.println(“发送时间:” + sf.format(new Date())); rabbitTemplate.convertAndSend(DelayedConfig.EXCHANGE_NAME, DelayedConfig.QUEUE_NAME, msg, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().setHeader(“x-delay”, 3000); return message; } }); }}3.3 消费消息import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.stereotype.Component;import java.text.SimpleDateFormat;import java.util.Date;@Component@RabbitListener(queues = “delayed.goods.order”)public class DelayedReceiver { @RabbitHandler public void process(String msg) { SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); System.out.println(“接收时间:” + sdf.format(new Date())); System.out.println(“消息内容:” + msg); }}3.4 测试队列import com.example.rabbitmq.RabbitmqApplication;import com.example.rabbitmq.mq.delayed.DelayedSender;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.text.SimpleDateFormat;import java.util.Date;@RunWith(SpringRunner.class)@SpringBootTestpublic class DelayedTest { @Autowired private DelayedSender sender; @Test public void Test() throws InterruptedException { SimpleDateFormat sf = new SimpleDateFormat(“yyyy-MM-dd”); sender.send(“Hi Admin.”); Thread.sleep(5 * 1000); //等待接收程序执行之后,再退出测试 }}执行结果如下:发送时间:2018-09-11 20:47:51接收时间:2018-09-11 20:47:54消息内容:Hi Admin.完整代码访问我的GitHub:https://github.com/vipstone/s…四、总结到此为止我们已经使用“rabbitmq-delayed-message-exchange”插件实现了延迟功能,但是需要注意的一点是,如果使用命令“rabbitmq-plugins disable rabbitmq_delayed_message_exchange”禁用了延迟插件,那么所有未发送的延迟消息都将丢失。 ...

November 16, 2018 · 1 min · jiezi

集成spring boot + mysql + docker实战

前言网上找过很多文章,关于通过docker构建mysql容器并将应用容器和docker容器关联起来的文章不多。本文将给出具体的范例。此处为项目的源码前置条件该教程要求在宿主机上配置了:dockermavenmysql容器新建一个mysql容器和别的教程没什么区别,这里我们将直接利用官方镜像来启动一个空的mysql容器。完整的内容位于mysql目录之下。只需要直接执行脚本sh start_mysql.sh即可启动一个包含位于container_demo数据库中的user表的数据库。使用语句docker exec -it demo_db mysql -u root -p可以进入容器中的mysql进程并查看我们的初始化情况。spring mvc之后就是初始化一个springmvc项目,同样的源码为src目录下,可以在github上看到。首先使用docker ps查看本地启动的mysql的端口号,并且修改application-dev.yml中的数据库信息。此时可以直接在idea总启动项目。比如这里我看到本地的端口号为32809,所以可以通过32809这个端口号直接访问数据库。在docker中使用的是test环境的配置,所以docker中的配置都应该写在test中。对源码在使用中的问题,欢迎留言或者提issue参考文章Spring Boot with Docker docker指令学习记录customize mysql dockerdocker安装mysql

November 15, 2018 · 1 min · jiezi

Spring Boot(十三)RabbitMQ安装与集成

一、前言RabbitMQ是一个开源的消息代理软件(面向消息的中间件),它的核心作用就是创建消息队列,异步接收和发送消息,MQ的全程是:Message Queue中文的意思是消息队列。<!–more–>1.1 使用场景削峰填谷:用于应对间歇性流量提升对于系统的“破坏”,比如秒杀活动,可以把请求先发送到消息队列在平滑的交由系统去处理,当访问量大于一定数量的时候,还可以直接屏蔽后续操作,给前台的用户友好的显示;延迟处理:可以进行事件后置,比如订单超时业务,用户下单30分钟未支付取消订单;系统解耦:消息队列也可以帮开发人员完成业务的解耦,比如用户上传头像的功能,最初的设计是用户上传完之后才能发帖,后面有增加了经验系统,需要在上传头像之后增加经验值,到后来又上线了金币系统,上传头像之后可以增加金币,像这种需求的不断升级,如果在业务代码里面写死每次该业务代码是很不优雅的,这个时候如果使用消息队列,那么只需要增加一个订阅器用于介绍用户上传头像的消息,再执行经验的增加和金币的增加是非常简单的,并且在不改动业务模块业务代码的基础上可以轻松实现,如果后期需要撤销某个模块了,只需要删除订阅器即可,就这样就降低了系统开发的耦合性;1.2 为什么使用RabbitMQ?现在市面上比较主流的消息队列还有Kafka、RocketMQ、RabbitMQ,它们的介绍和区别如下:Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache定级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。简单总结: Kafka的性能最好,适用于对消息吞吐量达,对消息丢失不敏感的系统;RocketMQ借鉴了Kafka并提高了消息的可靠性,修复了Kafka的不足;RabbitMQ性能略低于Kafka,并实现了AMQP(Advanced Message Queuing Protocol)高级消息队列协议的标准,有非常好的稳定性。支持语言对比RocketMQ 支持语言:Java、C++、GolangKafka 支持语言:Java、ScalaRabbitMQ 支持语言:C#、Java、Js/NodeJs、Python、Ruby、Erlang、Perl、Clojure、Golang1.3 RabbitMQ特点RabbitMQ的特点是易用、扩展性好(集群访问)、高可用,具体如下:可靠性:持久化、消息确认、事务等保证了消息的可靠性;伸缩性:集群服务,可以很方便的添加服务器来提高系统的负载;高可用:集群状态下部分节点出现问题依然可以运行;多语言支持:RabbitMQ几乎支持了所有的语言,比如Java、.Net、Nodejs、Golang等;易用的管理页面:RabbitMQ提供了易用了网页版的管理监控系统,可以很方便的完成RabbitMQ的控制和查看;插件机制:RabbitMQ提供了许多插件,可以丰富和扩展Rabbit的功能,用户也可编写自己的插件;1.4 RabbitMQ基础知识在了解消息通讯之前首先要了解3个概念:生产者、消费者和代理。生产者:消息的创建者,负责创建和推送数据到消息服务器;消费者:消息的接收方,用于处理数据和确认消息;代理:就是RabbitMQ本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。(一)消息发送原理首先你必须连接到Rabbit才能发布和消费消息,那怎么连接和发送消息的呢?你的应用程序和Rabbit Server之间会创建一个TCP连接,一旦TCP打开,并通过了认证,认证就是你试图连接Rabbit之前发送的Rabbit服务器连接信息和用户名和密码,有点像程序连接数据库,使用Java有两种连接认证的方式,后面代码会详细介绍,一旦认证通过你的应用程序和Rabbit就创建了一条AMQP信道(Channel)。信道是创建在“真实”TCP上的虚拟连接,AMQP命令都是通过信道发送出去的,每个信道都会有一个唯一的ID,不论是发布消息,订阅队列或者接收消息都是通过信道完成的。(二)为什么不通过TCP直接发送命令?对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条TCP会话,这就造成了TCP连接的巨大浪费,而且操作系统每秒能创建的TCP也是有限的,因此很快就会遇到系统瓶颈。如果我们每个请求都使用一条TCP连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因。(三)RabbitMQ名称解释ConnectionFactory(连接管理器): 应用程序与Rabbit之间建立连接的管理器,程序代码中使用;Channel(信道): 消息推送使用的通道;Exchange(交换器): 用于接受、分配消息;Queue(队列): 用于存储生产者的消息;RoutingKey(路由键): 用于把生成者的数据分配到交换器上;BindingKey(绑定键): 用于把交换器的消息绑定到队列上;看到上面的解释,最难理解的路由键和绑定键了,那么他们具体怎么发挥作用的,请看下图:1.5 交换器分类RabbitMQ的Exchange(交换器)分为四类:direct(默认)headersfanouttopic其中headers交换器允许你匹配AMQP消息的header而非路由键,除此之外headers交换器和direct交换器完全一致,但性能却很差,几乎用不到,所以我们这里不做解释。1.5.1 direct交换器direct为默认的交换器类型,也非常的简单,如果路由键匹配的话,消息就投递到相应的队列,如下图:1.5.2 fanout交换器fanout有别于direct交换器,fanout是一种发布/订阅模式的交换器,当你发送一条消息的时候,交换器会把消息广播到所有附加到这个交换器的队列上。注意: 对于fanout交换器来说routingKey(路由键)是无效的,这个参数是被忽略的。1.5.3 topic交换器topic交换器运行和fanout类似,但是可以更灵活的匹配自己想要订阅的信息,这个时候routingKey路由键就排上用场了,使用路由键进行消息(规则)匹配。topic路由器的关键在于定义路由键,定义routingKey名称不能超过255字节,使用“.”作为分隔符,例如:com.mq.rabbit.error。匹配规则匹配表达式可以用“”和“#”匹配任何字符,具体规则如下:“”匹配一个分段(用“.”分割)的内容;“#”匹配所有字符;例如发布了一个“cn.mq.rabbit.error”的消息:能匹配上的路由键:cn.mq.rabbit.cn.mq.rabbit.##.errorcn.mq.##不能匹配上的路由键:cn.mq.**.error1.6 消息持久化RabbitMQ队列和交换器有一个不可告人的秘密,就是默认情况下重启服务器会导致消息丢失,那么怎么保证Rabbit在重启的时候不丢失呢?答案就是消息持久化。当你把消息发送到Rabbit服务器的时候,你需要选择你是否要进行持久化,但这并不能保证Rabbit能从崩溃中恢复,想要Rabbit消息能恢复必须满足3个条件:投递消息的时候durable设置为true,消息持久化,代码:channel.queueDeclare(x, true, false, false, null),参数2设置为true持久化;设置投递模式deliveryMode设置为2(持久),代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),参数3设置为存储纯文本到磁盘;消息已经到达持久化交换器上;消息已经到达持久化的队列;持久化工作原理Rabbit会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit会把这条消息标识为等待垃圾回收。持久化的缺点消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用SSD硬盘可以使事情得到缓解,但他仍然吸干了Rabbit的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。所以使用者要根据自己的情况,选择适合自己的方式。学习更多RabbitMQ知识,访问:https://gitbook.cn/gitchat/ac…二、在Docker中安装RabbitMQ(1)下载镜像https://hub.docker.com/r/libr…alpine 轻量版management 带插件的版本从镜像的大小也可以很直观的看出来alpine是轻量版。使用命令:docker pull rabbitmq:3.7.7-management下载带management插件的版本。(2)运行RabbitMQ使用命令:docker run -d –hostname myrabbit –name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.7.7-management-d 后台运行–hostname 主机名称–name 容器名称-p 15672:15672 http访问端口,映射本地端口到容器端口-p 5672:5672 amqp端口,映射本地端口到容器端口正常启动之后,访问:http://localhost:15672/ 登录网页管理页面,用户名密码:guest/guest,登录成功如下图:三、RabbitMQ集成3.1 添加依赖如果用Idea创建新项目,可以直接在创建Spring Boot的时候,点击“Integration”面板,选择RabbitMQ集成,如下图:如果是老Maven项目,直接在pom.xml添加如下代码:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency>3.2 配置RabbitMQ信息在application.properties设置如下信息:spring.rabbitmq.host=localhostspring.rabbitmq.port=5672spring.rabbitmq.username=testspring.rabbitmq.password=test3.3 代码3.3 代码实现本节分别来看三种交换器:direct、fanout、topic的实现代码。3.3.1 Direct Exchange3.3.1.1 配置队列创建DirectConfig.java代码如下:package com.example.rabbitmq.mq;import org.springframework.amqp.core.Binding;import org.springframework.amqp.core.BindingBuilder;import org.springframework.amqp.core.DirectExchange;import org.springframework.amqp.core.Queue;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class DirectConfig { final static String QUEUE_NAME = “direct”; //队列名称 final static String EXCHANGE_NAME = “mydirect”; //交换器名称 @Bean public Queue queue() { // 声明队列 参数一:队列名称;参数二:是否持久化 return new Queue(DirectConfig.QUEUE_NAME, false); } // 配置默认的交换机,以下部分都可以不配置,不设置使用默认交换器(AMQP default) @Bean DirectExchange directExchange() { // 参数一:交换器名称;参数二:是否持久化;参数三:是否自动删除消息 return new DirectExchange(DirectConfig.EXCHANGE_NAME, false, false); } // 绑定“direct”队列到上面配置的“mydirect”路由器 @Bean Binding bindingExchangeDirectQueue(Queue directQueue, DirectExchange directExchange) { return BindingBuilder.bind(directQueue).to(directExchange).with(DirectConfig.QUEUE_NAME); }}3.3.1.2 发送消息创建Sender.java代码如下:package com.example.rabbitmq.mq;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.Date;/** * 消息发送者-生产消息 /@Componentpublic class Sender { @Autowired private AmqpTemplate rabbitTemplate; public void driectSend(String message) { System.out.println(“Direct 发送消息:” + message); //参数一:交换器名称,可以省略(省略存储到AMQP default交换器);参数二:路由键名称(direct模式下路由键=队列名称);参数三:存储消息 this.rabbitTemplate.convertAndSend(“direct”, message); }}注意:在direct交换器中,路由键名称就是队列的名称;发送消息“convertAndSend”的时候,第一个参数为交换器的名称,非必填可以忽略,如果忽略则会把消息发送到默认交换器“AMQP default”;3.3.1.3 消费消息创建Receiver.java代码如下:package com.example.rabbitmq.mq;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;/* * 消息接收者-消费消息 /@Component@RabbitListener(queues = “direct”)public class Receiver { @Autowired private AmqpTemplate rabbitTemplate; @RabbitHandler /* * 监听消费消息 / public void process(String message) { System.out.println(“Direct 消费消息:” + message); }}3.3.1.4 测试代码使用Spring Boot中的默认测试框架JUnit进行单元测试,不了解JUnit的可以参考我的上一篇文章,创建MQTest.java代码如下:package com.example.rabbitmq.mq;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.text.SimpleDateFormat;import java.util.Date;import static org.junit.Assert.;@RunWith(SpringRunner.class)@SpringBootTestpublic class MQTest { @Autowired private Sender sender; @Test public void driectTest() { SimpleDateFormat sf = new SimpleDateFormat(“yyyy-MM-dd”); sender.driectSend(“Driect Data:” + sf.format(new Date())); }}执行之后,效果如下图:表示消息已经被发送并被消费了。3.3.2 Fanout Exchange3.3.2.1 配置队列创建FanoutConfig.java代码如下:package com.example.rabbitmq.mq;import org.springframework.amqp.core.;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class FanoutConfig { final static String QUEUE_NAME = “fanout”; //队列名称 final static String QUEUE_NAME2 = “fanout2”; //队列名称 final static String EXCHANGE_NAME = “myfanout”; //交换器名称 @Bean public Queue queueFanout() { return new Queue(FanoutConfig.QUEUE_NAME); } @Bean public Queue queueFanout2() { return new Queue(FanoutConfig.QUEUE_NAME2); } //配置交换器 @Bean FanoutExchange fanoutExchange() { return new FanoutExchange(FanoutConfig.EXCHANGE_NAME); } // 绑定队列到交换器 @Bean Binding bindingFanoutExchangeQueue(Queue queueFanout, FanoutExchange fanoutExchange) { return BindingBuilder.bind(queueFanout).to(fanoutExchange); } // 绑定队列到交换器 @Bean Binding bindingFanoutExchangeQueue2(Queue queueFanout2, FanoutExchange fanoutExchange) { return BindingBuilder.bind(queueFanout2).to(fanoutExchange); }}3.3.2.2 发送消息创建FanoutSender.java代码如下:package com.example.rabbitmq.mq;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class FanoutSender { @Autowired private AmqpTemplate rabbitTemplate; public void send(String message) { System.out.println(“发送消息:” + message); this.rabbitTemplate.convertAndSend(FanoutConfig.EXCHANGE_NAME,FanoutConfig.QUEUE_NAME, message); } public void send2(String message) { System.out.println(“发送消息2:” + message); this.rabbitTemplate.convertAndSend(FanoutConfig.EXCHANGE_NAME,FanoutConfig.QUEUE_NAME2, message); }}3.3.2.3 消费消息创建两个监听类,第一个FanoutReceiver.java代码如下:package com.example.rabbitmq.mq;import com.rabbitmq.client.Channel;import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.stereotype.Component;import java.io.IOException;@Component@RabbitListener(queues = “fanout”)public class FanoutReceiver { @RabbitHandler public void process(String msg) { System.out.println(“Fanout(FanoutReceiver)消费消息:” + msg); }}第二个FanoutReceiver2.java代码如下:package com.example.rabbitmq.mq;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.stereotype.Component;@Component@RabbitListener(queues = “fanout2”)public class FanoutReceiver2 { @RabbitHandler public void process(String message) { System.out.println(“Fanout(FanoutReceiver2)消费消息:” + message); }}3.3.2.4 测试代码创建FanoutTest.java代码如下:package com.example.rabbitmq.mq;import com.example.rabbitmq.RabbitmqApplication;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.text.SimpleDateFormat;import java.util.Date;@RunWith(SpringRunner.class)@SpringBootTest(classes = RabbitmqApplication.class)public class FanoutTest { @Autowired private FanoutSender sender; @Test public void Test() throws InterruptedException { SimpleDateFormat sf = new SimpleDateFormat(“yyyy-MM-dd”); sender.send(“Time1 => " + sf.format(new Date())); sender.send2(“Date2 => " + sf.format(new Date())); }}运行测试代码,输出结果如下:发送消息:Time1 => 2018-09-11发送消息2:Date2 => 2018-09-11Fanout(FanoutReceiver2)消费消息:Time1 => 2018-09-11Fanout(FanoutReceiver2)消费消息:Date2 => 2018-09-11Fanout(FanoutReceiver)消费消息:Time1 => 2018-09-11Fanout(FanoutReceiver)消费消息:Date2 => 2018-09-11总结: 可以看出fanout会把消息分发到所有订阅到该交换器的队列,fanout模式是忽略路由键的。3.3.3 Topic Exchange3.3.3.1 配置队列@Configurationpublic class TopicConfig { final static String QUEUE_NAME = “log”; final static String QUEUE_NAME2 = “log.all”; final static String QUEUE_NAME3 = “log.all.error”; final static String EXCHANGE_NAME = “topicExchange”; //交换器名称 @Bean public Queue queuetopic() { return new Queue(TopicConfig.QUEUE_NAME); } @Bean public Queue queuetopic2() { return new Queue(TopicConfig.QUEUE_NAME2); } @Bean public Queue queuetopic3() { return new Queue(TopicConfig.QUEUE_NAME3); } // 配置交换器 @Bean TopicExchange topicExchange() { return new TopicExchange(TopicConfig.EXCHANGE_NAME); } // 绑定队列到交换器,并设置路由键(log.#) @Bean Binding bindingtopicExchangeQueue(Queue queuetopic, TopicExchange topicExchange) { return BindingBuilder.bind(queuetopic).to(topicExchange).with(“log.#”); } // 绑定队列到交换器,并设置路由键(log.) @Bean Binding bindingtopicExchangeQueue2(Queue queuetopic2, TopicExchange topicExchange) { return BindingBuilder.bind(queuetopic2).to(topicExchange).with(“log.”); } // 绑定队列到交换器,并设置路由键(log..error) @Bean Binding bindingtopicExchangeQueue3(Queue queuetopic3, TopicExchange topicExchange) { return BindingBuilder.bind(queuetopic3).to(topicExchange).with(“log..error”); }}3.3.3.2 发布消息@Componentpublic class TopicSender { @Autowired private AmqpTemplate rabbitTemplate; public void topicSender(String message) { String routingKey = “log.all.error”; System.out.println(routingKey + " 发送消息:” + message); this.rabbitTemplate.convertAndSend(TopicConfig.EXCHANGE_NAME, routingKey, message); }}3.3.3.3 消费消息@Component@RabbitListener(queues = “log”)public class TopicReceiver { @RabbitHandler public void process(String msg) { System.out.println(“log.# 消费消息:” + msg); }}@Component@RabbitListener(queues = “log.all”)public class TopicReceiver2 { @RabbitHandler public void process(String msg) { System.out.println(“log. 消费消息:” + msg); }}@Component@RabbitListener(queues = “log.all.error”)public class TopicReceiver3 { @RabbitHandler public void process(String msg) { System.out.println(“log..error 消费消息:” + msg); }}3.3.3.4 测试代码@RunWith(SpringRunner.class)@SpringBootTest(classes = RabbitmqApplication.class)public class FanoutTest { @Autowired private FanoutSender fanoutSender; @Test public void Test() { SimpleDateFormat sf = new SimpleDateFormat(“yyyy-MM-dd”); fanoutSender.send(“Time1 => " + sf.format(new Date())); fanoutSender.send2(“Date2 => " + sf.format(new Date())); }}输出结果:log.all.error 发送消息:time => 2018-09-11log.# 消费消息:time => 2018-09-11log..error 消费消息:time => 2018-09-11总结: 在Topic Exchange中“#”可以匹配所有内容,而“*”则是匹配一个字符段的内容。以上示例代码Github地址:https://github.com/vipstone/s…参考文档阿里 RocketMQ 优势对比:https://juejin.im/entry/5a0ab… ...

November 13, 2018 · 4 min · jiezi

Spring Cloud Feign Clients 无需 Controller自动暴露Restful接口

前言在开发SpringCloud应用中,Feign作为声明式调用的事实标准极大的简化了Rest远程调用,提供了类本地化的调用方式。服务提供方的接口暴露方式是通过Controller暴露Restful,而在这个Controller的代码现实中大部分都是处理请求然后再调用Service中的方法,是一个比较模板化的功能,但是工作量确不少。本文介绍一种通过动态代理的方式无需Controller直接暴露Restful接口。本文中使用笔者在Github开源的框架来实现,本文的讲解也在这个框架基础之上来说明Git路径:https://github.com/leecho/spr…依赖 <dependency> <groupId>com.github.leecho</groupId> <artifactId>spring-cloud-starter-feign-proxy</artifactId> <version>{last-version}</version> </dependency>实现定义Service Interface首先定义服务接口,使用@FeignClient标示是一个Feign接口,在这个示例Sevice中定义了CURD和文件上传方法,并使用了一些常用参数注解@Api(tags = “DemoService”, description = “Demo Feign Client”)@FeignClient(“demo-service”)public interface DemoService { /** * create demo * * @param demo * @return / @ApiOperation(value = “Create demo”) @PostMapping(value = “/demo”) Demo create(@RequestBody @Valid @ApiParam Demo demo); /* * update demo * * @param demo * @return / @PutMapping(value = “/demo”) Demo update(@RequestBody @Valid Demo demo); /* * delete demo * * @param id * @return / @DeleteMapping(value = “/demo/{id}”) Demo delete(@PathVariable(name = “id”) String id); /* * list demo * * @param id * @param headerValue test header value * @return / @GetMapping(value = “/demo/{id}”) Demo get(@PathVariable(name = “id”) String id, @RequestHeader(name = “header”) String headerValue); /* * list demos * * @return / @GetMapping(value = “/demos”) List<Demo> list(); /* * upload file * * @param file * @return */ @PostMapping(value = “/demo/upload”) String upload(@RequestPart(name = “file”) MultipartFile file);}实现Service在实现类中简单的实现了CURD和上传文件的方法@Slf4j@Primary@Servicepublic class DemoServiceImpl implements DemoService { @Override public Demo create(Demo demo) { log.info(“Create executed : " + demo); return demo; } @Override public Demo update(Demo demo) { log.info(“Update execute :” + demo); return demo; } @Override public Demo delete(String id) { log.info(“Delete execute : " + id); return Demo.builder().name(“demo-” + id).data(“data-” + id).build(); } @Override public Demo get(String id, String header) { Demo demo = Demo.builder() .name(header) .data(header).build(); System.out.println(“Get execute : " + id + “,” + header); return demo; } @Override public List<Demo> list() { System.out.println(“List execute”); List<Demo> demos = new ArrayList<>(); for (int i = 0; i < 5; i++) { Demo demo = Demo.builder() .name(“demo-” + i) .data(“data” + i).build(); demos.add(demo); } return demos; } @Override public String upload(MultipartFile file) { return file.getOriginalFilename(); }}动态生成Restful接口的原理是动态生成Controller类实现ServiceInterface,如果真正的实现类会被其他BEAN依赖,需要通过注解@Primary来方式来防止依赖冲突配置应用在应用中通过@EnableFeignProxies来标示应用在启动的过程中会扫描所有的FeignClients并暴露Restful接口@EnableFeignProxies(basePackages = “com.github.leecho”)@ComponentScan(“com.github.leecho”)@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}测试用例在测试用例中对Service的接口进行测试@SpringBootTest@RunWith(SpringJUnit4ClassRunner.class)@WebAppConfigurationpublic class DemoApplicationTest { @Autowired private WebApplicationContext context; private MockMvc mvc; @Before public void setUp() { mvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test public void create() throws Exception { mvc.perform(MockMvcRequestBuilders.post("/demo”) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(new ObjectMapper().writeValueAsString(Demo.builder().name(“create”).data(“data”).build())) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void update() throws Exception { mvc.perform(MockMvcRequestBuilders.put("/demo”) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(new ObjectMapper().writeValueAsString(Demo.builder().name(“update”).data(“data”).build())) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void delete() throws Exception { mvc.perform(MockMvcRequestBuilders.delete("/demo/{id}”, “1”) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void get() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/demo/{id}", “1”) .contentType(MediaType.APPLICATION_JSON_UTF8) .header(“header”, “header-value”) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void list() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/demos") .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void upload() throws Exception { mvc.perform(MockMvcRequestBuilders.multipart("/demo/upload").file(new MockMultipartFile(“file”, “test.txt”, “,multipart/form-data”, “upload test”.getBytes())) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); }}在启动日志中也可以看到Restful接口已经暴露成功。后语关于这个框架的介绍,后续详细的给大家进行介绍。文中所涉及到的代码也在Git中:spring-cloud-feign-proxy-sample ...

November 9, 2018 · 2 min · jiezi

Spring Boot(十一)Redis集成从Docker安装到分布式Session共享

一、简介Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API,Redis也是技术领域使用最为广泛的存储中间件,它是「Remote Dictionary Service」首字母缩写,也就是「远程字典服务」。<!–more–>Redis相比Memcached提供更多的数据类型支持和数据持久化操作。二、在Docker中安装Redis2.1 下载镜像访问官网:https://hub.docker.com/r/libr… 选择下载版本,本文选择最新Stable 4.0.11使用命令拉取镜像:docker pull redis:4.0.112.2 启动容器启动Redis命令如下:docker run –name myredis -p 6379:6379 -d redis:4.0.11 redis-server –appendonly yes命令说明:–name 设置别名-p 映射宿主端口到容器端口-d 后台运行redis-server –appendonly yes 在容器启动执行redis-server启动命令,打开redis持久化启动成功之后使用命令:docker ps查看redis运行请求,如下图为运行成功:2.3 使用客户端连接连接Redis不错的GUI工具应该是Redis Desktop Manager了,不过现在只有Linux版可以免费下载,我上传了一个Windows版本在百度云,版本号为:0.9.5(发布于2018.08.24)也是比较新的,链接: https://pan.baidu.com/s/16npZ… 密码: 9uqg,还是免安装的,很好用。Redis Desktop Manager客户端预览:三、Redis集成开发环境Spring Boot 2.0.4 RELEASEManven3.1 添加依赖在pom.xml添加如下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>注意不要依赖“spring-boot-starter-redis”它是旧版本,新版已经迁移到“spring-boot-starter-data-redis”了。3.2 配置Redis在application.properties进行如下设置:# Redis 配置# Redis服务器地址spring.redis.host=127.0.0.1# Redis服务器连接密码(默认为空)spring.redis.password=# Redis服务器连接端口spring.redis.port=6379 # Redis分片(默认为0)Redis默认有16个分片spring.redis.database=0# 连接池最大连接数(使用负值表示没有限制)spring.redis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.pool.max-wait=-1 # 连接池中的最大空闲连接spring.redis.pool.max-idle=8 # 连接池中的最小空闲连接spring.redis.pool.min-idle=0 # 连接超时时间(毫秒)spring.redis.timeout=10000# 指定spring的缓存为redisspring.cache.type=redis注意:spring.redis.timeout不要设置为0,设置为0查询Redis时会报错,因为查询连接时间太短了。3.3 Redis使用完成以上配置之后就可以写代码操作Redis了,示例代码如下:@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("/")public String doTest() { String _key = “time”; //缓存key stringRedisTemplate.opsForValue().set(_key, String.valueOf(new Date().getTime())); //redis存值 return stringRedisTemplate.opsForValue().get(_key); //redis取值}更多操作:stringRedisTemplate.opsForValue().set(“test”, “100”,60*10,TimeUnit.SECONDS); 向redis里存入数据和设置缓存时间;stringRedisTemplate.hasKey(“keyName”); 检查key是否存在,返回boolean;四、声明式缓存为了简化缓存可以直接使用声名式缓存,可以省去设置缓存和读取缓存的代码,使用起来会方便很多。声明式缓存使用步骤如下:4.1 设置Redis缓存在pom.xml文件设置缓存为Redis,代码如下:spring.cache.type=redis4.2 开启全局缓存在启动文件Application.java设置开启缓存,代码如下:@SpringBootApplication@EnableCachingpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}4.3 使用注解注解如下:@Cacheable 设置并读取缓存(第一次设置以后直接读取);@CachePut 更新缓存(每次删除并更新缓存结果);@CacheEvict 删除缓存(只删除缓存);通用属性:value 缓存名称;key 使用SpEL表达式自定义的缓存Key,比如:#name是以参数name为key的缓存,#resule.name是以返回结果的name作为key的缓存;4.3.1 @Cacheable 使用示例代码如下:// 缓存keyprivate final String _CacheKey = “userCacheKeyTime”; @RequestMapping("/")@Cacheable(value = _CacheKey)public String index() { System.out.println(“set cache”); return “cache:” + new Date().getTime();}只有首次访问的时候会在控制台打印“set cache”信息,之后直接返回Redis结果了,不会在有添加的打印信息出现。4.3.2 @CachePut 使用示例代码如下:// 缓存keyprivate final String _CacheKey = “userCacheKeyTime”;@RequestMapping("/put")@CachePut(value = _CacheKey)public String putCache() { System.out.println(“update cache”); return “update cache:” + new Date().getTime();}访问http://xxx/put 每次会把最新的数据存储缓存起来。4.3.3 @CacheEvict 使用示例代码如下:// 缓存keyprivate final String _CacheKey = “userCacheKeyTime”;@RequestMapping("/del")@CacheEvict(value = _CacheKey)public String delCache() { System.out.println(“缓存删除”); return “delete cache:” + new Date().getTime();}访问http://xxx/del 只会删除缓存,除此之后不会进行任何操作。五、分布式Session共享在分布式系统中Session共享有很多种方案,而把Session托管在缓存中是最常用的方案之一,下面来看Session在Redis中的托管步骤。5.1 添加依赖在pom.xml中添加如下引用:<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId></dependency>5.2 开启Session功能在启动类Application.java的类注解添加开启Session,代码如下:@SpringBootApplication@EnableCaching@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)public class RedisApplication { public static void main(String[] args) { SpringApplication.run(RedisApplication.class, args); }}其中maxInactiveIntervalInSeconds为Session过期时间,默认30分钟,设置单位为秒。5.3 Session使用接下来编写一段代码来测试一下Session,示例代码如下:@RequestMapping("/uid")public String testSession(HttpSession session) { UUID uid = (UUID) session.getAttribute(“uid”); if (uid == null) { uid = UUID.randomUUID(); } session.setAttribute(“uid”, uid); return session.getId();}连续访问两次请求之后,查看控制台信息如下图:可以看出,两次访问的SessionId是一样的,这个时候在查看Redis 客户端,如下图:发现Redis里存储的Session过期时间也是对的,符合我们的设置。5.4 分布式系统共享Session因为把Session托管给同一台Redis服务器了,所以Session在Spring Boot中按照如上方式在配置多台服务器,得到的Session是一样的。示例源码下载:https://github.com/vipstone/s…参考资料Spring boot中Redis的使用:http://www.ityouknow.com/spri… ...

November 3, 2018 · 1 min · jiezi

Spring Boot(十)Logback和Log4j2集成与日志发展史

一、简介Java知名的日志有很多,比如:JUL、Log4j、JCL、SLF4J、Logback、Log4j2,那么这些日志框架之间有着怎样的关系?诞生的原因又是解决什么问题?下面一起来看。<!–more–>1.1 JULJava有自己的日志框架JUL(Java Util Logging)在java.util.logging下,因为对开发者不友好,使用成本太高和日志级别分类不清晰的问题,所有很少有开发者用。1.2 Log4j因为JUL的缺陷问题,这就给了Log4j机会,所有Log4j一经推出就迅速风靡全球。1.3 JCLJCL是Jakarta Commons-Logging的缩写,Jakarta在这里指的是一个组织,而不是印度的首都雅加达,Jakarta,一个早期的Apache开源项目,用于管理各个Java子项目,诸如Tomcat, Ant, Maven, Struts, JMeter, Velocity, JMeter, Commons等。2011年12月,在所有子项目都被迁移为独立项目后,Jakarta名称就不再使用了。JCL诞生的初衷是因为Java自身的一些包用了JUL,而Log4j用户使用的有很多,那么JCL就是提供一套API来实现不同Logger之间的切换。1.4 SLF4JSLF4J(Simple Logging Facade For Java)简单日志门面,和JCL功能类似,但JCL有一个致命的缺点就是算法复杂,出现问题难以排除,而SLF4J的诞生就是为了解决JCL的缺点。值得一提的是SLF4J的作者就是Log4j的作者。1.5 LogbackLogback是Log4j的作者的另一个开源日志组件,与Log4j相比,Logback重新了内核,使它的性能提升了很多,大约是Log4j的10倍,同时占用更小的内存,并且完整的实现了SLF4J API是你可以很方便的切换日志框架。1.6 Log4j2Log4j2有着和Logback相同的功能,但又有自己单用的功能,比如:插件式结构、配置文件优化、异步日志等。Log4j2是Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些固有问题。从GitHub的更新日志来看,Logback已经有半年没有更新了,而作为知名组织的Apache下的Log4j2的更新却是非常活跃的,Log4j 1.x 于2015年8月停止维护更新了。GitHub地址Logback:https://github.com/qos-ch/log…log4j2:https://github.com/apache/log…本文分别来看Logback和Log4j2在Spring Boot中的实现。二、Logback使用开发环境JDK 8Spring Boot 2.0.4 RELEASEMavenWindows 10IDEA 2018.22.1 Logback的使用Spring Boot默认集成了Logback,可以开箱即用,非常方便。因为spring-boot-starter-logging是Logback的日志实现,而Spring Boot启动项spring-boot-starter又依赖了spring-boot-starter-logging,所以Spring Boot就默认集成了Logback,包依赖如下图:日志是默认控制台输出的,我们程序启动的时候就使用Logback,如下图所示:日志组成解读:日期和时间:毫秒精度,易于排序日志级别:trace、debug、info、warn、error(日志级别依次从低到高)进程ID—分隔符线程名称:括在方括号中(可以截断控制台输出)记录器名称:这通常是源类名(通常缩写)日志具体信息2.2 输入文件如果需要输出日志到文件,只需要在application.properties配置文件设置:logging.file或logging.path,示例如下:logging.level.root=infologging.file=D:\log\my.log可以通过设置日志的级别,忽略更低级别的日志输出。注意: logging.file和logging.path设置一个属性即可,如果两个都设置,则以logging.file为主,logging.path无效。日志文件容量设置:使用“logging.file.max-history”属性为日志最大容量设置,默认10M超过则分割为多个文件。2.3 自定义日志配置日志服务在ApplicationContext创建前就初始化了,所以通过设置属性和传统的配置XML文件,可以对日志进行管理和控制。只需要在src/main/resources下,创建好约定名称的XML文件,即可完成对日志系统的设置,不同的日志系统有不同的约定名称,如下列表:日志名称logbacklogback-spring.xml, logback-spring.groovy, logback.xml, 或者 logback.groovylog4j2log4j2-spring.xml 或者 log4j2.xmlSpring Boot官方建议使用“-spring”的命名规则,进行日志配置,如:logback-spring.xml而不是logback.xml。当然你也可以自定义日志名称,只需要在application.properties配置即可,代码如下:logging.config=classpath:logging-config.xml来看一个logback-spring.xml示例文件:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <!– 日志根目录–> <springProperty scope=“context” name=“LOG_HOME” source=“logging.path” defaultValue="/data/logs/spring-boot-logback"/> <!– 日志级别 –> <springProperty scope=“context” name=“LOG_ROOT_LEVEL” source=“logging.level.root” defaultValue=“DEBUG”/> <!– 标识这个"STDOUT" 将会添加到这个logger –> <springProperty scope=“context” name=“STDOUT” source=“log.stdout” defaultValue=“STDOUT”/> <!– 日志文件名称–> <property name=“LOG_PREFIX” value=“spring-boot-logback” /> <!– 日志文件编码–> <property name=“LOG_CHARSET” value=“UTF-8” /> <!– 日志文件路径+日期–> <property name=“LOG_DIR” value="${LOG_HOME}/%d{yyyyMMdd}" /> <!–对日志进行格式化–> <property name=“LOG_MSG” value="- | [%X{requestUUID}] | [%d{yyyyMMdd HH:mm:ss.SSS}] | [%level] | [${HOSTNAME}] | [%thread] | [%logger{36}] | –> %msg|%n “/> <!–文件大小,默认10MB–> <property name=“MAX_FILE_SIZE” value=“50MB” /> <!– 配置日志的滚动时间 ,表示只保留最近 10 天的日志–> <property name=“MAX_HISTORY” value=“10”/> <!–输出到控制台–> <appender name=“STDOUT” class=“ch.qos.logback.core.ConsoleAppender”> <!– 输出的日志内容格式化–> <layout class=“ch.qos.logback.classic.PatternLayout”> <pattern>${LOG_MSG}</pattern> </layout> </appender> <!–输出到文件–> <appender name=“0” class=“ch.qos.logback.core.rolling.RollingFileAppender”> </appender> <!– 定义 ALL 日志的输出方式:–> <appender name=“FILE_ALL” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <!–日志文件路径,日志文件名称–> <File>${LOG_HOME}/all_${LOG_PREFIX}.log</File> <!– 设置滚动策略,当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB –> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <!–日志文件路径,新的 ALL 日志文件名称,“ i ” 是个变量 –> <FileNamePattern>${LOG_DIR}/all_${LOG_PREFIX}%i.log</FileNamePattern> <!– 配置日志的滚动时间 ,表示只保留最近 10 天的日志–> <MaxHistory>${MAX_HISTORY}</MaxHistory> <!–当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB–> <timeBasedFileNamingAndTriggeringPolicy class=“ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP”> <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!– 输出的日志内容格式化–> <layout class=“ch.qos.logback.classic.PatternLayout”> <pattern>${LOG_MSG}</pattern> </layout> </appender> <!– 定义 ERROR 日志的输出方式:–> <appender name=“FILE_ERROR” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <!– 下面为配置只输出error级别的日志 –> <filter class=“ch.qos.logback.classic.filter.LevelFilter”> <level>ERROR</level> <OnMismatch>DENY</OnMismatch> <OnMatch>ACCEPT</OnMatch> </filter> <!–日志文件路径,日志文件名称–> <File>${LOG_HOME}/err_${LOG_PREFIX}.log</File> <!– 设置滚动策略,当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB –> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <!–日志文件路径,新的 ERR 日志文件名称,“ i ” 是个变量 –> <FileNamePattern>${LOG_DIR}/err_${LOG_PREFIX}%i.log</FileNamePattern> <!– 配置日志的滚动时间 ,表示只保留最近 10 天的日志–> <MaxHistory>${MAX_HISTORY}</MaxHistory> <!–当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB–> <timeBasedFileNamingAndTriggeringPolicy class=“ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP”> <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!– 输出的日志内容格式化–> <layout class=“ch.qos.logback.classic.PatternLayout”> <Pattern>${LOG_MSG}</Pattern> </layout> </appender> <!– additivity 设为false,则logger内容不附加至root ,配置以配置包下的所有类的日志的打印,级别是 ERROR–> <logger name=“org.springframework” level=“ERROR” /> <logger name=“org.apache.commons” level=“ERROR” /> <logger name=“org.apache.zookeeper” level=“ERROR” /> <logger name=“com.alibaba.dubbo.monitor” level=“ERROR”/> <logger name=“com.alibaba.dubbo.remoting” level=“ERROR” /> <!– ${LOG_ROOT_LEVEL} 日志级别 –> <root level="${LOG_ROOT_LEVEL}"> <!– 标识这个”${STDOUT}“将会添加到这个logger –> <appender-ref ref="${STDOUT}”/> <!– FILE_ALL 日志输出添加到 logger –> <appender-ref ref=“FILE_ALL”/> <!– FILE_ERROR 日志输出添加到 logger –> <appender-ref ref=“FILE_ERROR”/> </root></configuration>2.4 代码中使用日志在代码中使用日志,只需要使用如下代码:private Logger logger = LoggerFactory.getLogger(this.getClass());//…logger.debug(“this is debug”);logger.info(“this is info”);三、Log4j2集成3.1 配置依赖组件Spring Boot添加Log4j2依赖的同时,需要排除Logback依赖,配置pom.xml代码如下:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions><!– 去掉logback配置 –> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <!– 引入log4j2依赖 –> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency></dependencies>3.2 自定义日志配置添加log4j2-spring.xml文件在src/main/resources文件下,配置文件代码如下:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <Appenders> <Console name=“CONSOLE” target=“SYSTEM_OUT”> <PatternLayout charset=“UTF-8” pattern="[%-5p] %d %c - %m%n" /> </Console> <File name=“File” fileName=“D:\mylog.log”> <PatternLayout pattern="%m%n" /> </File> </Appenders> <Loggers> <root level=“info”> <AppenderRef ref=“CONSOLE” /> <AppenderRef ref=“File” /> </root> </Loggers></configuration>输入日志到控制台和D盘mylog.log文件中。到此为止,已经完成了log4j2的集成,运行项目,查看日志。示例源码:https://github.com/vipstone/s…参考资料JAVA日志的前世今生:https://www.cnblogs.com/xiexj… ...

October 30, 2018 · 2 min · jiezi

分享几个 SpringBoot 实用的小技巧

前言最近分享的一些源码、框架设计的东西。我发现大家热情不是特别高,想想大多数应该还是正儿八经写代码的居多;这次就分享一点接地气的: SpringBoot 使用中的一些小技巧。算不上多高大上的东西,但都还挺有用。屏蔽外部依赖第一个是屏蔽外部依赖,什么意思呢?比如大家日常开发时候有没有这样的烦恼:项目是基于 SpringCloud 或者是 dubbo 这样的分布式服务,你需要依赖许多基础服务。比如说某个订单号的生成、获取用户信息等。由于服务拆分,这些功能都是在其他应用中以接口的形式提供,单测还好我还可以利用 Mock 把它屏蔽掉。但如果自己想把应用启动起来同时把自己相关的代码跑一遍呢?通常有几种做法:本地把所有的服务都启动起来。把注册中心换为开发环境,依赖开发环境的服务。直接把代码推送到开发环境自测。看起来三种都可以,以前我也是这么干的。但还是有几个小问题:本地启动有可能服务很多,全部起来电脑能不能撑住还两说,万一服务有问题就进行不下去了。依赖开发环境的前提是网络打通,还有一个问题就是开发环境代码很不稳定很大可能会影响你的测试。推送到开发环境应该是比较靠谱的方案,但如果想调试只有日志大法,没有本地 debug 的效率高效。那如何解决问题呢?既可以在本地调试也不用启动其他服务。其实也可以利用单测的做法,把其他外部依赖 Mock 掉就行了。大致的流程分为以下几步:SpringBoot 启动之后在 Spring 中找出你需要屏蔽的那个 API 的 bean(通常情况下这个接口都是交给 Spring 管理的)。手动从 bean 容器中删除该 bean。重新创建一个该 API 的对象,只不过是通过 Mock 出来的。再手动注册进 bean 容器中。以下面这段代码为例: @Override public BaseResponse<OrderNoResVO> getUserByHystrix(@RequestBody UserReqVO userReqVO) { OrderNoReqVO vo = new OrderNoReqVO(); vo.setAppId(123L); vo.setReqNo(userReqVO.getReqNo()); BaseResponse<OrderNoResVO> orderNo = orderServiceClient.getOrderNo(vo); return orderNo; }这是一个 SpringCloud 应用。它依赖于 orderServiceClient 获取一个订单号。其中的 orderServiceClient 就是一个外部 API,也是被 Spring 所管理。替换原有的 Bean下一步就是替换原有的 Bean。@Componentpublic class OrderMockServiceConfig implements CommandLineRunner { private final static Logger logger = LoggerFactory.getLogger(OrderMockServiceConfig.class); @Autowired private ApplicationContext applicationContext; @Value("${excute.env}") private String env; @Override public void run(String… strings) throws Exception { // 非本地环境不做处理 if (“dev”.equals(env) || “test”.equals(env) || “pro”.equals(env)) { return; } DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); OrderServiceClient orderServiceClient = defaultListableBeanFactory.getBean(OrderServiceClient.class); logger.info("======orderServiceClient {}=====", orderServiceClient.getClass()); defaultListableBeanFactory.removeBeanDefinition(OrderServiceClient.class.getCanonicalName()); OrderServiceClient mockOrderApi = PowerMockito.mock(OrderServiceClient.class, invocationOnMock -> BaseResponse.createSuccess(DateUtil.getLongTime() + “”, “mock orderNo success”)); defaultListableBeanFactory.registerSingleton(OrderServiceClient.class.getCanonicalName(), mockOrderApi); logger.info("======mockOrderApi {}=====", mockOrderApi.getClass()); }}其中实现了 CommandLineRunner 接口,可以在 Spring 容器初始化完成之后调用 run() 方法。代码非常简单,简单来说首先判断下是什么环境,毕竟除开本地环境其余的都是需要真正调用远程服务的。之后就是获取 bean 然后手动删除掉。关键的一步:OrderServiceClient mockOrderApi = PowerMockito.mock(OrderServiceClient.class, invocationOnMock -> BaseResponse.createSuccess(DateUtil.getLongTime() + “”, “mock orderNo success”));defaultListableBeanFactory.registerSingleton(OrderServiceClient.class.getCanonicalName(), mockOrderApi);创建了一个新的 OrderServiceClient 对象并手动注册进了 Spring 容器中。第一段代码使用的是 PowerMockito.mock 的 API,他可以创建一个代理对象,让所有调用 OrderServiceClient 的方法都会做默认的返回。BaseResponse.createSuccess(DateUtil.getLongTime() + “”, “mock orderNo success”))测试一下,当我们没有替换时调用刚才那个接口并且本地也没有启动 OrderService:因为没有配置 fallback 所以会报错,表示找不到这个服务。替换掉 bean 时:再次请求没有报错,并且获得了我们默认的返回。通过日志也会发现 OrderServiceClient 最后已经被 Mock 代理了,并不会去调用真正的方法。配置加密下一个则是配置加密,这应该算是一个基本功能。比如我们配置文件中的一些账号和密码,都应该是密文保存的。因此这次使用了一个开源组件来实现加密与解密,并且对 SpringBoot 非常友好只需要几段代码即可完成。首先根据加密密码将需要加密的配置加密为密文。替换原本明文保存的配置。再使用时进行解密。使用该包也只需要引入一个依赖即可:<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>1.14</version></dependency>同时写一个单测根据密码生成密文,密码也可保存在配置文件中:jasypt.encryptor.password=123456接着在单测中生成密文。 @Autowired private StringEncryptor encryptor; @Test public void getPass() { String name = encryptor.encrypt(“userName”); String password = encryptor.encrypt(“password”); System.out.println(name + “—————-”); System.out.println(password + “—————-”); }之后只需要使用密文就行。由于我这里是对数据库用户名和密码加密,所以还得有一个解密的过程。利用 Spring Bean 的一个增强接口即可实现:@Componentpublic class DataSourceProcess implements BeanPostProcessor { @Autowired private StringEncryptor encryptor; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof DataSourceProperties){ DataSourceProperties dataSourceProperties = (DataSourceProperties) bean; dataSourceProperties.setUsername(encryptor.decrypt(dataSourceProperties.getUsername())) ; dataSourceProperties.setPassword(encryptor.decrypt(dataSourceProperties.getPassword())); return dataSourceProperties ; } return bean; }}这样就可以在真正使用时还原为明文。同时也可以在启动命令中配置刚才的密码:java -Djasypt.encryptor.password=password -jar target/jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar总结这样两个小技巧就讲完了,大家有 SpringBoot 的更多使用技巧欢迎留言讨论。上文的一些实例代码可以在这里找到:https://github.com/crossoverJie/springboot-cloud欢迎关注公众号一起交流: ...

October 15, 2018 · 2 min · jiezi

Spring Boot (八)MyBatis + Docker + MongoDB 4.x

一、MongoDB简介1.1 MongoDB介绍MongoDB是一个强大、灵活,且易于扩展的通用型数据库。MongoDB是C++编写的文档型数据库,有着丰富的关系型数据库的功能,并在4.0之后添加了事务支持。随着存储数据量不断的增加,开发者面临一个困难:如何扩展数据库?而扩展数据库分为横向扩展和纵向扩展,纵向扩展就是使用计算能力更强大的机器,它的缺点就是:机器性能的提升有物理极限的制约,而且大型机通常都是非常昂贵的,而MongoDB的设计采用的是横向扩展的模式,面向文档的数据模型使它很容易的在多台服务器上进行数据分割。MongoDB能自动处理夸集群的数据和负载,自动重新分配文档,这样开发者就能集中精力编写应用程序,而不需要考虑如果扩展的问题。<!–more–>1.2 MongoDB安装MongoDB的安装简单来说分为两种:官网下载对应物理机的安装包,直接安装使用Docker镜像,安装到Docker上推荐使用第二种,直接使用MongoDB镜像安装到Docker上,这样带来的好处是:安装简单、方便,且快速更容易进行数据迁移,使用Docker可以很容易的导入和导出整个MongoDB到任何地方所以本文将重点介绍MongoDB在Docker上的安装和使用。如果想要直接在物理机安装Docker,可以查看我之前的一篇文章《MongoDB基础介绍安装与使用》:https://www.cnblogs.com/vipst…1.3 Docker上安装MongoDB在Docker上安装软件一般需要两步:pull(下载)对应的镜像(相对于下载软件)装载镜像到容器(相对于安装软件)1.3.1 下载镜像下载镜像,需要到镜像市场:https://hub.docker.com/,如要要搜索的软件“mongo”,选择官方镜像“Official”,点击详情,获取相应的下载方法,我们得到下载MongoDB的命令如下:docker pull mongo:latest1.3.2 装载镜像到容器使用命令:docker run –name mongodb1 -p 27018:27017 -d mongo:latest–name 指定容器名称-p 27018:27017 映射本地端口27018到容器端口27017-d 后台运行mongo:latest 镜像名称和标签使用“docker images”查看镜像名称和标签,如下图:容器装载成功之后,就可以使用Robo 3T客户端进行连接了,是不需要输入用户名和密码的,如下图:表示已经连接成功了。Robo 3T为免费的连接MongoDB的数据库工具,可以去官网下载:https://robomongo.org/download1.3.3 开启身份认证如果是生成环境,没有用户名和密码的MongoDB是非常不安全的,因此我们需要开启身份认证。Setp1:装载容器我们还是用之前下载的镜像,重新装载一个容器实例,命令如下:docker run –name mongodb2 -p 27019:27017 -d mongo:latest –auth其中“–auth”就是开启身份认证。装载完身份认证成功容器之后,我们需要进入容器内部,给MongoDB设置用户名和密码。Setp2:进入容器内部docker exec -it <容器id/名称> bashSetp3:进入mongo命令行模式mongo adminSetp4:创建用户db.createUser({ user: ‘admin’, pwd: ‘admin’, roles: [ { role: “userAdminAnyDatabase”, db: “admin” } ] });创建的用户名为“admin”密码为“admin”,指定的数据库为“admin”。这个时候,我们使用Robo 3T 输入相应的信息进行连接,如下图:表示已经连接成功了。1.3.4 创建数据库设置用户上面我们用“admin”账户使用了系统数据库“admin”,通常在生成环境我们不会直接使用系统的数据库,这个时候我们需要自己创建自己的数据库分配相应的用户。Setp1:首先需要进入容器docker exec -it <容器id/名称> bashSetp2:创建数据库use testdb如果没有testdb就会自动创建数据库。Setp3:创建用户分配数据库db.createUser({ user: ‘admin’, pwd: ‘admin’, roles: [ { role: “readWrite”, db: “testdb” } ] });其中 role: “readWrite” 表式给用户赋值操作和读取的权限,当然增加索引、删除表什么的也是完全没有问题的。到目前为止我们就可以使用admin/admin操作testdb数据库了。1.3.5 其他Docker命令删除容器:docker container rm <容器id/名称>停止容器:docker stop <容器id/名称>启动容器:docker start <容器id/名称>查看运行是容器:docker ps查询所有的容器:docker ps -a二、MyBatis集成MongoDBSpring Boot项目集成MyBatis前两篇文章已经做了详细的介绍,这里就不做过多的介绍,本文重点来介绍MongoDB的集成。Setp1:添加依赖在pom.xml添加如下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId></dependency>Setp2:配置MongoDB连接在application.properties添加如下配置:spring.data.mongodb.uri=mongodb://username:pwd@172.16.10.79:27019/testdbSetp3:创建实体类import java.io.Serializable;public class User implements Serializable { private Long id; private String name; private int age; private String pwd; //…略set、get}Setp4:创建Dao类import com.hello.springboot.entity.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.data.mongodb.core.query.Update;import org.springframework.stereotype.Component;import java.util.List;@Componentpublic class UserDao { @Autowired private MongoTemplate mongoTemplate; /** * 添加用户 * @param user User Object / public void insert(User user) { mongoTemplate.save(user); } /* * 查询所有用户 * @return / public List<User> findAll() { return mongoTemplate.findAll(User.class); } /* * 根据id 查询 * @param id * @return / public User findById(Long id) { Query query = new Query(Criteria.where(“id”).is(id)); User user = mongoTemplate.findOne(query, User.class); return user; } /* * 更新 * @param user / public void updateUser(User user) { Query query = new Query(Criteria.where(“id”).is(user.getId())); Update update = new Update().set(“name”, user.getName()).set(“pwd”, user.getPwd()); mongoTemplate.updateFirst(query, update, User.class); } /* * 删除对象 * @param id / public void deleteUserById(Long id) { Query query = new Query(Criteria.where(“id”).is(id)); mongoTemplate.remove(query, User.class); }}Setp4:创建Controllerimport com.hello.springboot.dao.IndexBuilderDao;import com.hello.springboot.dao.UserDao;import com.hello.springboot.entity.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.ModelAndView;@RestController@RequestMapping("/")public class UserController { @Autowired private UserDao userDao; @RequestMapping("/") public ModelAndView index() { User user = new User(); user.setId(new Long(1)); user.setAge(18); user.setName(“Adam”); user.setPwd(“123456”); userDao.insert(user); ModelAndView modelAndView = new ModelAndView("/index"); modelAndView.addObject(“count”, userDao.findAll().size()); return modelAndView; }}Setp5:创建页面代码<html><head> <title>王磊的博客</title></head><body>Hello ${count}</body></html>到此为止已经完成了MongoDB的集成,启动项目,输入“http://localhost:8080/”去数据库查看插入的数据吧。正常插入数据库如下图:三、MongoDB主键自增细心的用户可能会发现,虽然MongoDB已经集成完了,但插入数据库的时候user的id是手动set的值,接下来我们来看怎么实现MongoDB中的id自增。3.1 实现思路MongoDB 实现id自增和Spring Boot JPA类似,是在数据库创建一张表,来记录表的“自增id”,只需要保证每次都增加的id和返回的id的原子性,就能保证id实现“自增”的功能。3.2 实现方案有了思路之后,接下来我们来看具体的实现方案。3.2.1 创建实体类import org.springframework.data.annotation.Id;import org.springframework.data.mongodb.core.mapping.Document;@Document(collection = “IndexBuilder”)public class IndexBuilder { @Id private String id; private Long seq; //..省略get、set方法}其中collection = “IndexBuilder"是指数据库的集合名称,对应关系型数据库的表名。3.2.2 创建Dao类import com.hello.springboot.entity.IndexBuilder;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoOperations;import org.springframework.data.mongodb.core.query.Update;import org.springframework.stereotype.Component;import static org.springframework.data.mongodb.core.FindAndModifyOptions.options;import static org.springframework.data.mongodb.core.query.Criteria.where;import static org.springframework.data.mongodb.core.query.Query.query;@Componentpublic class IndexBuilderDao { @Autowired private MongoOperations mongo; /* * 查询下一个id * @param collectionName 集合名 * @return */ public Long getNextSequence(String collectionName) { IndexBuilder counter = mongo.findAndModify( query(where("_id”).is(collectionName)), new Update().inc(“seq”, 1), options().returnNew(true).upsert(true), IndexBuilder.class); return counter.getSeq(); }}3.2.3 使用“自增”的idUser user = new User();user.setId(indexBuilderDao.getNextSequence(“user”));//…其他设置核心代码:indexBuilderDao.getNextSequence(“user”) 使用“自增”的id,实现id自增。到此为止,已经完成了MongoDB的自增功能,如果使用正常,数据库应该是这样的:数据库的IndexBuilder就是用来记录每个集合的“自增id”的。MongoDB集成的源码:https://github.com/vipstone/s… ...

October 8, 2018 · 2 min · jiezi

Spring Boot (七)MyBatis代码自动生成和辅助插件

一、简介1.1 MyBatis Generator介绍MyBatis Generator 是MyBatis 官方出品的一款,用来自动生成MyBatis的 mapper、dao、entity 的框架,让我们省去规律性最强的一部分最基础的代码编写。1.2 MyBatis Generator使用MyBatis Generator的使用方式有4种:命令行生成Maven方式生成使用Ant任务生成使用Java代码生成其中推荐使用Maven方式进行代码生成,因为集成和使用比较简单。<!–more–>1.3 开发环境MySQL:8.0.12MyBatis Generator:1.3.7Maven:4.0IDEA:2018.2二、代码自动生成配置上面介绍了使用MyBatis Generator的几种方式,其中最推荐使用的是Maven方式,所以下面我们来看Maven方式的MyBatis代码生成,分为四步:Step1:添加依赖配置pom.xml文件,增加依赖和配置生成文件(“generatorConfig.xml”)路径:<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.12</version> </dependency> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> </dependency> </dependencies> <executions> <execution> <id>Generate MyBatis Artifacts</id> <phase>package</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <!–允许移动生成的文件 –> <verbose>true</verbose> <!– 是否覆盖 –> <overwrite>true</overwrite> <!– 自动生成的配置 –> <configurationFile>generatorConfig.xml</configurationFile> </configuration></plugin>Step2:添加配置文件根据上面在pom里的配置,我们需要添加generatorConfig.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> <!–加载配置文件,为下面读取数据库信息准备–> <properties resource=“application.properties”/> <!–defaultModelType=“flat” 大数据字段,不分表 –> <context id=“Mysql” targetRuntime=“MyBatis3Simple” defaultModelType=“flat”> <property name=“autoDelimitKeywords” value=“true” /> <property name=“beginningDelimiter” value="" /&gt; &lt;property name="endingDelimiter" value="” /> <property name=“javaFileEncoding” value=“utf-8” /> <plugin type=“org.mybatis.generator.plugins.SerializablePlugin” /> <plugin type=“org.mybatis.generator.plugins.ToStringPlugin” /> <!– 注释 –> <commentGenerator > <property name=“suppressAllComments” value=“true”/><!– 是否取消注释 –> <property name=“suppressDate” value=“true” /> <!– 是否生成注释代时间戳–> </commentGenerator> <!–数据库链接地址账号密码–> <jdbcConnection driverClass="${spring.datasource.driver-class-name}" connectionURL="${spring.datasource.url}" userId="${spring.datasource.username}" password="${spring.datasource.password}"> </jdbcConnection> <!– 类型转换 –> <javaTypeResolver> <!– 是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.) –> <property name=“forceBigDecimals” value=“false”/> </javaTypeResolver> <!–生成Model类存放位置–> <javaModelGenerator targetPackage=“com.hello.springboot.entity” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> <property name=“trimStrings” value=“true”/> </javaModelGenerator> <!– 生成mapxml文件 –> <sqlMapGenerator targetPackage=“mapper” targetProject=“src/main/resources/mybatis” > <property name=“enableSubPackages” value=“false” /> </sqlMapGenerator> <!– 生成mapxml对应client,也就是接口dao –> <javaClientGenerator targetPackage=“com.hello.springboot.dao” targetProject=“src/main/java” type=“XMLMAPPER” > <property name=“enableSubPackages” value=“false” /> </javaClientGenerator> <table tableName=“article” enableCountByExample=“true” enableUpdateByExample=“true” enableDeleteByExample=“true” enableSelectByExample=“true” selectByExampleQueryId=“true”> <generatedKey column=“id” sqlStatement=“Mysql” identity=“true” /> </table> <table tableName=“user_log” enableCountByExample=“true” enableUpdateByExample=“true” enableDeleteByExample=“true” enableSelectByExample=“true” selectByExampleQueryId=“true”> <generatedKey column=“id” sqlStatement=“Mysql” identity=“true” /> </table> </context></generatorConfiguration>其中数据库连接的配置,是从application.properties直接读取的。Step3:配置全局属性文件全局属性文件application.properties的配置,和Spring Boot增加MyBatis的配置是一样的,如果你的Spring Boot项目里面已经配置了MyBatis支持,请忽略此步骤。# MyBatis 配置spring.datasource.url=jdbc:mysql://172.16.10.79:3306/mytestdb?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=truespring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.cj.jdbc.Drivermybatis.type-aliases-package=com.hello.springboot.mappermybatis.config-locations=classpath:mybatis/mybatis-config.xmlmybatis.mapper-locations=classpath:mybatis/mapper/*.xml注意: MySQL 6以后JDBC的配置就不一样了,参照如上MySQL 8的配置。Step4:点击Maven生成代码如果你使用的是IDEA,点击最右侧的Maven Projects => 点击mybatis-generator => 右键mybatis-generator:generate => Run Maven Build,如下图所示:正常控制台输出“BUILD SUCCESS”说明生成已经成功了,如果出现错误,根据错误提示信息排除处理错误即可。MyBatis Generator 示例源码:https://github.com/vipstone/s…三、安装IDEA插件如果你使用的是 IDEA,那么强烈建议你安装一款免费的IDEA插件“Free MyBatis plugin”,可以实现dao到mapper xml对应方法的快速映射,点击任意一个快速调整到相应的方法,提高工作效率,效果如下图所示:点击绿色的箭头直接跳转到了mapper xml对应的方法了,如下图所示:可以相互点击,进行对应的跳转。安装步骤点击菜单栏Flie => Settings点击Browse repostitories..输入“Free MyBatis plugin”查找插件点击安装,重启IDEA关键步骤的截图如下:四、总结使用了MyBatis Generator可以帮我们自动生成实体类,和5个最基础的方法,大大的提高我们的工作效率,用户只需要按需写自己独有的一些业务即可。同时增加“Free MyBatis plugin”插件,可以很方便的帮我们开发和调试代码,真是实实在在的福利。 ...

September 30, 2018 · 2 min · jiezi

springboot整合vue小试牛刀

序本文主要研究一下如何在springboot工程整合vuemaven <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>新建springboot的web工程,默认会在resources目录下生成static以及templates文件夹templates文件用于存放后端渲染的模板,这里我们采用前后端分离的方式,因而该文件夹就没有用了static文件夹就是存放静态文件的地方plugin <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!– mvn process-resources –> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy Vue.js frontend content</id> <phase>generate-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>src/main/resources/static</outputDirectory> <overwrite>true</overwrite> <resources> <resource> <directory>${basedir}/vue-demo/dist</directory> <includes> <include>static/</include> <include>index.html</include> </includes> </resource> </resources> </configuration> </execution> </executions> </plugin> </plugins> </build>这里我们使用了maven-resources-plugin插件,将vue工程npm run build之后的dist文件夹下的文件拷贝到static目录下这里我们假设vue工程名为vue-demo,在这个springboot工程的根目录下对于vue工程,首先执行npm run build生成静态文件,之后对maven工程执行mvn process-resources,就可以一键拷贝小结在springboot工程整合vue的话,将静态文件拷贝到src/main/resources/static目录下即可,这样就可以在springboot工程打开静态文件了,对api的请求也无需再转发,也没有跨域问题,比较适合管理后台前端资源的整合。docA Lovely Spring View: Spring Boot & Vue.js

September 24, 2018 · 1 min · jiezi

Spring Boot 最佳实践(五)Spring Data JPA 操作 MySQL 8

一、Spring Data JPA 介绍JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Spring Data JPA是在 Hibernate 基础上封装的一款框架。开发环境Spring Boot 2.0.4Spring Data JPA 2.0.4MySQL 8.0.12JDK 8IDEA 2018.2Windows 10<!–more–>二、集成步骤2.1 配置依赖添加Spring Data JPA 和 MySQL Connector,配置pom.xml文件,代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>2.0.4.RELEASE</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.12</version></dependency>更多JPA版本:http://mvnrepository.com/arti… 更多Mysql版本:http://mvnrepository.com/arti…2.2 application.properties 设置配置文件## 数据源配置spring.datasource.url=jdbc:mysql://172.16.10.79:3306/mytestdb?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=truespring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.jpa.hibernate.ddl-auto=updatespring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialectspring.jpa.show-sql=truehbm2ddl.auto:自动创建|更新|验证数据库表结构dialect:设置数据库引擎为InnoDBshow-sql:打印sql语句,方便调试hbm2ddl.auto有四个属性:create:每次加载 hibernate 时都会删除上一次的生成的表,然后根据你的 model 类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。[删除-创建-操作]create-drop :每次加载 hibernate 时根据 model 类生成表,但是 sessionFactory 一关闭,表就自动删除。[删除-创建-操作-再删除]update:最常用的属性,第一次加载 hibernate 时根据 model 类会自动建立起表的结构(前提是先建立好数据库),以后加载 hibernate 时根据 model 类自动更新表结构,即使表结构改变了,但表中的行仍然存在,不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。[没表-创建-操作 | 有表-更新没有的属性列-操作]validate:每次加载 hibernate 时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。[启动验证表结构,验证不成功,项目启动失败]2.3 增加实体类(Entity)@Entitypublic class User implements Serializable { @Id @GeneratedValue private Long id; @Column(name = “name”, nullable = false) private String name; @Column(nullable = false) private int age; @Column(nullable = false) private String pwd; public User(){} public User(String name, int age, String pwd) { this.name = name; this.age = age; this.pwd = pwd; } //…忽略set、get方法}@GeneratedValue 自动生成id@Column 设置列属性(name=“数据库列名”)@Transient 不会映射到数据库2.4 创建 Repository 接口构建业务方法public interface UserRepository extends JpaRepository<User,Long> { public User findByName(String name);}继承JpaRepository之后就继承了:Repository.save(user); // 插入或保存Repository.saveFlush(user); // 保存并刷新Repository.exists(1) // 主键查询是否存在Repository.findOne(1); // 主键查询单条Repository.delete(1); // 主键删除Repository.findByUsername(“stone”); // 查询单条Repository.findAll(pageable); // 带排序和分页的查询列表Repository.saveState(1, 0); // 更新单个字段这些方法,可以不写一行代码就可以实现对一个表的操作,当然你也可以扩展一些自己的方法,只需要在UserRepository里面添加方法即可。2.5 添加、查询数据库@Controller@RequestMapping("/")public class UserController { @Autowired private UserRepository userRepository; @RequestMapping("/") public ModelAndView index() { userRepository.save(new User(“老王”,18,“123456”)); ModelAndView modelAndView = new ModelAndView("/index"); modelAndView.addObject(“dataSize”, userRepository.findAll().size()); return modelAndView; }}到现在为止,集成 Spring Data JPA 已经全部完成了,启动调试,查看运行效果吧。三、高级使用本节高级使用将会涉及的知识点如下:事务实现根据名称自动生成SQL自定义Sql语句查询3.1 事务实现3.1.1 Spring事务实现步骤实现事务,只需要两步即可:步骤一、在application.properties配置数据库引擎为InnoDB:spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect步骤二、在方法或类上标识事务@Transactional示例代码:@Transactionalpublic void saveGroup(){ userRepository.save(user); userRepository.save(user2);}如果出现错误,就会进行事务回滚。3.1.2 事务不生效的原因3.1.2.1 确认数据库引擎在application.properties配置数据库引擎为InnoDB:spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect3.1.2.2 查看表的引擎必须为InnoDB通过命令:show table status from mytestdb;修改表的引擎:alter table table_name engine=innodb;3.1.2.3 注意引入@Transactional的命名空间@Transactional注解来自org.springframework.transaction.annotation包,而不是javax.transaction.3.2 根据名称自动生成SQLJPA支持根据简单的关键字自动生成Sql查询的方法,比如根据name和age的组合查询,代码如下:public User findByNameAndAge(String name,int age);使用关键字“And”即可,或者查询时间区间的:public User findByStartDateBetween(Long startDate);使用关键字“Between”即可。更多内部支持的关键字,如下表:KeywordSampleJPQL snippetAndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2Is,EqualsfindByFirstname,findByFirstnameIs… where x.firstname = ?1BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2LessThanfindByAgeLessThan… where x.age < ?1LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1GreaterThanfindByAgeGreaterThan… where x.age > ?1GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1AfterfindByStartDateAfter… where x.startDate > ?1BeforefindByStartDateBefore… where x.startDate < ?1IsNullfindByAgeIsNull… where x.age is nullIsNotNull,NotNullfindByAge(Is)NotNull… where x.age not nullLikefindByFirstnameLike… where x.firstname like ?1NotLikefindByFirstnameNotLike… where x.firstname not like ?1StartingWithfindByFirstnameStartingWith… where x.firstname like ?1(parameter bound with appended %)EndingWithfindByFirstnameEndingWith… where x.firstname like ?1(parameter bound with prepended %)ContainingfindByFirstnameContaining… where x.firstname like ?1(parameter bound wrapped in %)OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname descNotfindByLastnameNot… where x.lastname <> ?1InfindByAgeIn(Collection<Age> ages)… where x.age in ?1NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1TruefindByActiveTrue()… where x.active = trueFalsefindByActiveFalse()… where x.active = falseIgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)官方文档:https://docs.spring.io/spring…3.3 自定义Sql语句查询对于用户自己编写sql,Spring Boot JPA也有很好的支持,只需要添加@Query(sql)即可。示例代码:@Transactional@Modifying@Query(“update User set name=?1 where id=?2”)public int modifyName(String name,Long id);注意:在执行修改和删除的时候必须添加@Modifying注解,ORM才知道要执行写操作,update/delete query 的时候,也必须需要加上@Transactional(事务)才能正常操作。四、常见错误在 Spring Data JPA 的使用当中,可能会遇到如下的一些错误。1.No default constructor for entity实体类Entity没有空参数的默认构造函数,新增即可解决。2.java.sql.SQLException: Access denied for user ‘’@‘172.17.0.1’ (using password: NO)启动项目报错,用户名和密码配置的key有误,MySQL8的用户名和密码配置和之前的不一样,MySQL 8 正确的用户名密码配置如下:spring.datasource.username=rootspring.datasource.password=123456# 以下为配置老数据库驱动配置#spring.datasource.data-username=root#spring.datasource.data-password=1234563.Caused by: java.lang.IllegalStateException: Cannot load driver class: com.mysql.jdbc.DriverMySQL 8 的spring.datasource.driver-class-name配置需要改为“com.mysql.cj.jdbc.Driver”而不是“com.mysql.jdbc.Driver”,正确配置如下:spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ...

September 7, 2018 · 2 min · jiezi

Spring Boot 最佳实践(三)模板引擎FreeMarker集成

一、FreeMaker介绍FreeMarker是一款免费的Java模板引擎,是一种基于模板和数据生成文本(HMLT、电子邮件、配置文件、源代码等)的工具,它不是面向最终用户的,而是一款程序员使用的组件。FreeMarker最初设计是用来在MVC模式的Web开发中生成HTML页面的,所以没有绑定Servlet或任意Web相关的东西上,所以它可以运行在非Web应用环境中。发展史FreeMarker第一版在1999年未就发布了,2002年初使用JavaCC(Java Compiler Compiler是一个用Java开发的语法分析生成器)重写了FreeMarker的核心代码,2015年FreeMarker代码迁移到了Apache下。GitHub地址:https://github.com/apache/fre…工作原理FreeMarker模板存储在服务器上,当有用户访问的时候,FreeMarker会查询出相应的数据,替换模板中的标签,生成最终的HTML返回给用户,如下图:二、FreeMarker基础使用基础使用分为3部分,这3部分组成了FreeMarker:指令表达式指令是FreeMarker用来识别转换的特殊标签,表达式是标签里具体的语法实现,其他部分是一些不好分类的模板。2.1 指令使用FTL(freemarker template language)标签来调用指令。指令速览:assignattempt, recovercompressescape, noescapeflushftlfunction, returnglobalif, else, elseifimportincludelist, else, items, sep, breaklocalmacro, nested, returnnoparsentsettingstopswitch, case, default, breakt, lt, rtvisit, recurse, fallback用户自定义标签下来我们分别来看每个指令对应具体使用。2.1.1 assign 代码声明assign 分为变量和代码片段声明两种。2.1.1.1 变量声明可以是单变量声明,或多变量声明,下面是多变量声明的示例:<#assign name=“adam” age=18 “sex”=“man”>${name} - ${age} - ${“sex”}单个变量的话,只写一个就可以了。2.1.1.2 代码片段声明<#assign code> <#list [“java”,“golang”] as c> ${c} </#list></#assign>${code}其中 ${code} 是用来执行方法的,如果不调用话,代码片段不会执行。2.1.2 attempt, recover 异常指令attempt(尝试), recover(恢复)指令类似于程序的try catch,示例如下:<#attempt> i am ${name} <#recover> error name</#attempt>如果有变量“name”就会正常显示,显示“i am xxx”,如果没有变量就会显示“error name”。2.1.3 compress 压缩代码移除空白行<#compress> 1 2 3 4 5 test only I said, test only</#compress>1 2 3 4 5test onlyI said, test only效果如下:对空白不敏感的格式,移除空白行还是挺有用的功能。2.1.4 escape, noescape 转义,不转义2.1.4.1 escape使用<#escape x as x?html> ${firstName} ${lastName}</#escape>上面的代码,类似于:${firstName?html}${lastName?html}Java代码:@RequestMapping("/")public ModelAndView index() { ModelAndView modelAndView = new ModelAndView("/index"); modelAndView.addObject(“firstName”, “<span style=‘color:red’>firstName</span>”); modelAndView.addObject(“lastName”, “lastName”); return modelAndView;}最终的效果是:2.1.4.2 “?html”语法解析单问号后面跟的是操作函数,类似于Java中的方法名,html属于内建函数的一个,表示字符串会按照HTML标记输出,字符替换规则如下:< 替换为 &lt;> 替换为 &gt;& 替换为 &amp;" 替换为 &quot;2.1.4.3 noescape使用HTML代码:<#escape x as x?html> <#noescape> ${firstName} </#noescape> ${lastName}</#escape>Java代码:@RequestMapping("/")public ModelAndView index() { ModelAndView modelAndView = new ModelAndView("/index"); modelAndView.addObject(“firstName”, “<span style=‘color:red’>firstName</span>”); modelAndView.addObject(“lastName”, “lastName”); return modelAndView;}最终效果:2.1.5 function, return 方法声明代码格式:<#function name param1 param2 … paramN> … <#return returnValue> …</#function>name 为方法名称param1, param2,paramN 方法传递过来的参数,可以有无限个参数,或者没有任何参数return 方法返回的值,可以出现在function的任何位置和出现任意次数示例代码如下:<#function sum x y z> <#return x+y+z></#function>${sum(5,5,5)}注意:function如果没有return是没有意义的,相当于返回null,而function之中信息是不会打印到页面的,示例如下:<#function wantToPrint> 这里的信息是显示不了的</#function><#if wantToPrint()??> Message:${wantToPrint()}</#if>“??”用于判断值是否是null,如果为null是不执行的。如果不判null直接使用${}打印,会报模板错误,效果如下:2.1.6 global 全局代码声明语法如下:<#global name=value>或<#global name1=value1 name2=value2 … nameN=valueN>或<#global name> capture this</#global>global使用和assign用法类似,只不过global声明是全局的,所有的命名空间都是可见的。2.1.7 if elseif else 条件判断语法如下:<#if condition> …<#elseif condition2> …<#elseif condition3> ……<#else> …</#if>示例如下:<#assign x=1 ><#if x==1> x is 1<#elseif x==2> x is 2<#else> x is not 1</#if>2.1.8 import 引入模板语法: <#import path as hash>示例如下footer.ftl 代码如下:<html><head> <title>王磊的博客</title></head><body>this is footer.ftl<#assign copy=“来自 王磊的博客”></body></html>index.ftl 代码如下:<html><head> <title>王磊的博客</title></head><body><#import “footer.ftl” as footer>${footer.copy}</body></html>最终输出内容:来自 王磊的博客 2.1.9 include 嵌入模板语法: <#include path> 示例如下footer.ftl 代码如下:<html><head> <title>王磊的博客</title></head><body>this is footer.ftl<#assign copy=“来自 王磊的博客”></body></html>index.ftl 代码如下:<html><head> <title>王磊的博客</title></head><body><#include “footer.ftl”></body></html>最终内容如下:this is footer.ftl2.1.10 list, else, items, sep, break 循环2.1.10.1 正常循环输出1-3的数字,如果等于2跳出循环,代码如下:<#list 1..3 as n> ${n} <#if n==2> <#break> </#if></#list>注意:“1..3”等于[1,2,3]。结果: 1 22.1.10.2 使用items输出示例如下:<#list 1..3><ul><#items as n> <li>${n}</li></#items></ul></#list>2.1.10.3 sep 使用跳过最后一项<#list 1..3 as n> ${n} <#sep>,</#sep></#list>最终结果:1 , 2 , 32.1.10.4 数组最后一项代码如下:<#list 1..3 as n> ${n} <#if !n_has_next> 最后一项 </#if></#list>使用“变量_has_next”判断是否还有下一个选项,来找到最后一项,最终的结果:1 2 3 最后一项 2.1.11 macro 宏宏:是一个变量名的代码片段,例如:<#macro sayhi name> Hello, ${name}</#macro><@sayhi “Adam” />相当于声明了一个名称为“sayhi”有一个参数“name”的宏,使用自定义标签“@”调用宏。输出的结果: Hello, Adam2.1.12 switch, case, defalut, break 多条件判断示例代码如下:<#assign animal=“dog” ><#switch animal> <#case “pig”> This is pig <#break> <#case “dog”> This is dog <#break> <#default> This is Aaimal</#switch>2.1.13 扩展知识指令自动忽略空格特性FreeMarker会忽略FTL标签中的空白标记,所以可以直接写:<#list [“老王”,“老李”,“老张”] as p> ${p}</#list>即使是这个格式也是没有任何问题的,FreeMarker会正常解析。2.2 表达式2.2.1 字符串拼接字符拼接代码:<#assign name=“ABCDEFG”>${“Hello, ${name}"}结果:Hello, ABCDEFG2.2.2 算术运算2.2.2.1 算术符算术符有五种:+-/% 求余(求模)示例代码:${100 - 10 * 20}输出:-1002.2.2.2 数值转换${1.999?int}输出:1注意:数值转换不会进行四舍五入,会舍弃小数点之后的。2.2.3 内建函数(重点)内建函数:相当于我们Java类里面的内置方法,非常常用,常用的内建函数有:时间内建函数、字符内建函数、数字内建函数等。2.2.3.1 单个问号和两个问号的使用和区别单问号:在FreeMarker中用单个问号,来调用内建函数,比如: ${“admin”?length} 查看字符串“admin”的字符长度,其中length就是字符串的内建函数。双引号:表示用于判断值是否为null,比如:<#if admin??> Admin is not null</#if>2.2.3.2 字符串内建函数2.2.3.2.1 是否包含判断使用contains判断,代码示例:<#if “admin”?contains(“min”)> min<#else >not min</#if>输出:min2.2.3.2.2 大小写转换示例代码:<#assign name=“Adam”>${name?uncap_first} ${name?upper_case}${name?cap_first}${name?lower_case}输出:adam ADAM Adam adam更多的字符串内建函数:https://freemarker.apache.org…2.2.3.3 数字内建函数示例代码:${1.23569?string.percent}${1.23569?string[“0.##”]}${1.23569?string[“0.###”]}输出:124% 1.24 1.236注意:使用string.percent计算百分比,会自动四舍五入。使用“?string[“0.##"]”可以自定义取小数点后几位,会自动四舍五入。2.2.3.4 时间内建函数2.2.3.4.1 时间戳转换为任何时间格式代码:<#assign timestamp=1534414202000>${timestamp?number_to_datetime?string[“yyyy/MM/dd HH:mm”]}输出:2018/08/16 18:10 2.2.3.4.2 时间格式化示例代码:<#assign nowTime = .now>${nowTime} <br />${nowTime?string[“yyyy/MM/dd HH:mm”]} <br />输出:2018-8-16 18:33:50 2018/08/16 18:33 更多内建方法:https://freemarker.apache.org…三、Spring Boot 集成3.1 集成环境Spring Boot 2.0.4FreeMaker 2.3.28JDK 8Windows 10IDEA 2018.2.13.2 集成步骤3.2.1 pom.xml 添加FreeMaker依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency>3.2.2 application.properties 配置模板主要配置,如下:## Freemarker 配置spring.freemarker.template-loader-path=classpath:/templates/spring.freemarker.cache=falsespring.freemarker.charset=UTF-8spring.freemarker.check-template-location=truespring.freemarker.content-type=text/htmlspring.freemarker.expose-request-attributes=falsespring.freemarker.expose-session-attributes=falsespring.freemarker.request-context-attribute=requestspring.freemarker.suffix=.ftl配置项类型默认值建议值说明spring.freemarker.template-loader-pathStringclasspath:/templates/默认模版存放路径spring.freemarker.cachebooltrue默认是否开启缓存,生成环境建议开启spring.freemarker.charsetString-UTF-8编码spring.freemarker.content-typeStringtext/htmltext/htmlcontent-type类型spring.freemarker.suffixString.ftl.ftl模板后缀spring.freemarker.expose-request-attributesboolfalsefalse设定所有request的属性在merge到模板的时候,是否要都添加到model中spring.freemarker.expose-session-attributesboolfalsefalse设定所有HttpSession的属性在merge到模板的时候,是否要都添加到model中.spring.freemarker.request-context-attributeString-requestRequestContext属性的名称更多配置:# FREEMARKER (FreeMarkerProperties)spring.freemarker.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.spring.freemarker.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.spring.freemarker.cache=false # Whether to enable template caching.spring.freemarker.charset=UTF-8 # Template encoding.spring.freemarker.check-template-location=true # Whether to check that the templates location exists.spring.freemarker.content-type=text/html # Content-Type value.spring.freemarker.enabled=true # Whether to enable MVC view resolution for this technology.spring.freemarker.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.spring.freemarker.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.spring.freemarker.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring’s macro library, under the name “springMacroRequestContext”.spring.freemarker.prefer-file-system-access=true # Whether to prefer file system access for template loading. File system access enables hot detection of template changes.spring.freemarker.prefix= # Prefix that gets prepended to view names when building a URL.spring.freemarker.request-context-attribute= # Name of the RequestContext attribute for all views.spring.freemarker.settings.= # Well-known FreeMarker keys which are passed to FreeMarker’s Configuration.spring.freemarker.suffix=.ftl # Suffix that gets appended to view names when building a URL.spring.freemarker.template-loader-path=classpath:/templates/ # Comma-separated list of template paths.spring.freemarker.view-names= # White list of view names that can be resolved.3.2.3 编写HTML代码<html><head> <title>王磊的博客</title></head><body><div> Hello,${name}</div></body></html>3.2.4 编写Java代码新建index.java文件,Application.java(入口文件)代码不便,index.java代码如下:import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;@Controller@RequestMapping(”/")public class Index { @RequestMapping(”/") public ModelAndView index() { ModelAndView modelAndView = new ModelAndView("/index"); modelAndView.addObject(“name”, “老王”); return modelAndView; }}关键代码解读:@Controller注解:标识自己为控制器,只需要配置@RequestMapping之后,就可以把用户URL映射到控制器;使用ModelAndView对象,指定视图名&添加视图对象。3.2.5 运行执行上面4个步骤之后,就可以运行这个Java项目了,如果是IDEA使用默认快捷键“Shift + F10”启动调试,在页面访问:http://localhost:8080/ 就可看到如下效果:四、参考资料FreeMarker官方文档:https://freemarker.apache.org/FreeMarker翻译的中文网站:http://freemarker.foofun.cn/t… ...

August 30, 2018 · 3 min · jiezi