飞天班第12节:SpringBoot开发单体应用(下)

2020/03/22

1、精通Swagger

什么是swagger

官网:https://swagger.io/

swagger是一个Api框架,

  • Restful api自动生成文档,和代码对应的在线Api文档。
  • 可以直接运行测试接口,不用下载postman。
  • 支持多种开发语言,java、php等。

现在的很多项目都是前后端分离的敏捷方式开发:

  • 前端–>前端的控制层、视图层 (专业的前端团队开发)
  • 后端–>后端的控制层、服务层、数据访问层(专业的后端团队开发)

前后端的交互都是通过API来进行的,怎么处理API的约定?

早期:后端编写文档(协同文档),前端根据文档调用接口获取数据然后渲染视图。

但是随着项目的推进,后端每次修改功能,都要同步更新文档,跟前端团队协商,增加了项目的时间成本,最终可能导致项目延时。使用swagger就很好的解决了开发团队的API协同问题,同时解放了后端人员写Api文档的痛苦。

集成swagger

基础集成

1、导入依赖

maven仓库搜swagger2

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>2.9.2</version>
</dependency>
<!--swagger默认ui-->
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.9.2</version>
</dependency>

2、编写配置

@Configuration
@EnableSwagger2
public class SwaggerConfig {

	// 注册bean Docker,可以注册多个来分组
  @Bean
	public Docket demoDocket(){
		Docket docket = new Docket(DocumentationType.SWAGGER_2).groupName("demo");
		return docket;
	}
  
  @Bean
	public Docket adminDocket(){
		Docket docket = new Docket(DocumentationType.SWAGGER_2).groupName("admin");
		return docket;
	}
}

点进DocumentationType源码,你会发现有3个版本,我们现在使用的是swagger2版本

3、开启注解@EnableSwagger2,启动测试

对,集成就这么简单,启动后访问http://localhost:8080/swagger-ui.html#/

配置Swagger

1、配置Docket文档信息

点进Docket源码,默认的文档信息

 public Docket(DocumentationType documentationType) {
        this.apiInfo = ApiInfo.DEFAULT;
        this.groupName = "default";
        this.enabled = true;
        this.genericsNamingStrategy = new DefaultGenericTypeNamingStrategy();
        this.applyDefaultResponseMessages = true;
        this.host = "";
        this.pathMapping = Optional.absent();
        this.apiSelector = ApiSelector.DEFAULT;
        this.enableUrlTemplating = false;
        this.vendorExtensions = Lists.newArrayList();
        this.documentationType = documentationType;
    }

点进ApiInfo源码,默认的api信息

 public static final Contact DEFAULT_CONTACT = new Contact("", "", "");
 public static final ApiInfo DEFAULT = new ApiInfo("Api Documentation", "Api Documentation", "1.0", "urn:tos",
          DEFAULT_CONTACT, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList<VendorExtension>());

所以默认的什么都不配,就会返回上面的文档信息

2、配置接口扫描,需要哪些被扫描到文档中

@Bean
public Docket demoDocket(){
  return new Docket(DocumentationType.SWAGGER_2)
    .groupName("demo")
    .apiInfo(demoApiinfo())
    .select()
    .apis(RequestHandlerSelectors.basePackage("com.jude.demo"))// 包路径扫描到文档中
    .build();
}

private ApiInfo demoApiinfo(){
  // 建造者模式
  return new ApiInfoBuilder()
    .contact(new Contact("icoding","https://www.icodingedu.com/","icoding666@qq.com"))   // 作者联系方式
    .version("1.0.0")   // 文档版本号
    .description("测试使用")    // 简介
    .title("艾编程-测试API文档")   // 标题
    .termsOfServiceUrl("https://www.icodingedu.com/my/course/52")   // 服务链接
    .build();
}

RequestHandlerSelectors的其他几个方法说明

any()	// 扫描所有接口
none()	// 不扫描接口
basePackage	// 根据包路径扫描
withMethodAnnotation(final Class<? extends Annotation> annotation)	// 通过方法上的注解扫描,如PostMapping.class,GetMapping.class,ApiOpertion.class
withClassAnnotation(final Class<? extends Annotation> annotation)	// 通过类上的注解扫描,如Api.class

3、配置哪些接口不被扫描

@Bean
	public Docket demoDocket(){
		return new Docket(DocumentationType.SWAGGER_2)
				.groupName("demo")
				.apiInfo(demoApiinfo())
				.select()
				.apis(RequestHandlerSelectors.basePackage("com.jude.demo"))     // 包路径扫描到文档中
				.paths(PathSelectors.ant("/demo/**"))// 只扫描/demo/开头的请求,实现过滤
				.build();
	}

PathSelectors的其他方法说明

PathSelectors.any()	// 任何请求都会扫描
PathSelectors.regex()	// 以正则表达式匹配

也可以使用Predicates,常用的有not(),or(),and()

.paths(Predicates.or(PathSelectors.ant("/demo/dept/**"),PathSelectors.ant("/demo/employee/**")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))

配置Swagger的开关

test 、dev环境才显示swgger的Docket,通过org.springframework.core.env.Environment接口判断当前环境的profiles,就知道当前生效的环境了。

swagger的Api注解

实体类上的配置

@ApiModel 用于参数实体类说明

@ApiModelProperty 字段说明

@Data
@ApiModel(value = "数据采集爬取的内容字段提交对象")
public class WebCollectContentDTO {
	@ApiModelProperty(value = "采集ID")
	private Long collectId;

	@ApiModelProperty(value = "内容页url地址")
	private String pageUrl;
}

接口上的配置

@Api 用在controller类上,标注具体实现内容

@ApiOperation 用在方法,表示一个http请求的操作

@ApiParam 用在方法,参数说明

@Api(tags = "部门接口")
public class DepartmentController {
  
    @ApiOperation("测试接口")
    @PostMapping("/test")
    public String hello(@ApiParam(value = "这个名字会返回",required = true) String username){
        return username;
    }
}

皮肤包

默认使用springfox-swagger-ui,访问localhost:8080/swagger-ui.html

maven仓库上搜swagger-ui,推荐使用swagger-bootstrap-ui 访问localhost:8080/doc.html

<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>swagger-bootstrap-ui</artifactId>
  <version>1.9.6</version>
</dependency>

整合YapiUpload

swagger2可以生成在线API文档,但需要项目启动后才能访问查看,如果需要统一管理项目的API文档,则需要借助Yapi。YApi是高效、易用、功能强大的API管理平台,目前在github是开源的: https://github.com/YMFE/yapi

YApi不仅提供了常用的接口管理功能,还提供了权限管理、Mock数据、Swagger数据导入等功能

官方文档 https://hellosean1025.github.io/yapi

1、需要先安装nodejs和MongoDB

安装nodejs


wget https://nodejs.org/dist/v12.16.1/node-v12.16.1-linux-x64.tar.gz
tar -zxvf node-v12.16.1-linux-x64.tar.gz
# 创建软链接
ln -s ~/node-12.16.1/bin/node /usr/bin/node
ln -s ~/node-12.16.1/bin/npm /usr/bin/npm
node -v

使用docker或者压缩包的方式安装mongodb

mkdir mongodb
tar zxvf mongodb-linux-x86_64-3.0.6.tgz -C mongodb
mv mongodb-linux-x86_64-3.0.6/* /usr/local/mongodb

# 增加mongodb环境变量
vi /etc/profile
# export PATH=$PATH:/usr/local/mongodb/bin

# 生效环境变量
source /etc/profile

#检查mongodb环境变量是否生效
mongo --version

#配置mongodb配置文件信息
cd mongodb
mkdir data
vim mongodb.cnf

#配置信息详情
# 指定数据存储目录 需要提前创建
dbpath=/usr/local/mongodb/data/
# 指定日志文件
logpath=/usr/local/mongodb/data/mongo.log   
# 日志追加写    
logappend=true 
# 创建后台子进程
fork=true
# 指定端口号
port=27017
#配置信息详情

#启动mongodbserver
mongod -f /usr/local/mongodb/mongdb.cnf

#连接本机的mongodb
cd /usr/local/mongodb/bin/
# 进入客户端
mongo

#当前所有数据库
>show dbs
#创建用户名/密码
>db.createUser({user:'root',pwd:'xxx', roles:[{role:'userAdminAnyDatabase', db:'admin'}]})

2、安装Yapi

这里可以选择两种方式,建议nodejs的方式安装,方便

  • tar安装包

    # 解压安装包
    tar -xvf yapi.tar
    cp vendors/config_example.json ./config.json
      
    # 配置config.json
    {
      "port": "3000",
      "adminAccount": "admin@admin.com",
      "db": {
        "servername": "127.0.0.1",
        "DATABASE": "yapi",
        "port": 27017,
        "user": "root",
        "pass": "xxx",
        "authSource": "admin"
      },
      "mail": {
        "enable": true,
        "host": "smtp.exmail.qq.com",
        "port": 465,
        "from": "xxx@xxx.cn",
        "auth": {
          "user": "xxx@xxx.cn",
          "pass": "xxx"
        }
      }
    }
      
    #初始化数据库
    cd vendors
    npm run install-server
      
    #启动yapi server
    node server/app.js
    
  • npm方式

    npm install -g yapi-cli --registry https://registry.npm.taobao.org
    yapi server 
    

    执行 yapi server 启动可视化部署程序,输入相应的配置和点击开始部署,就能完成整个网站的部署。部署完成之后,可按照提示信息,执行 node/{网站路径/server/app.js} 启动服务器。在浏览器打开指定url, 点击登录输入您刚才设置的管理员邮箱,默认密码为 ymfe.org 登录系统(默认密码可在个人中心修改)。

    利用pm2管理yapi服务

    npm install pm2 -g  //安装pm2
    cd  {项目目录}
    pm2 start "vendors/server/app.js" --name yapi //pm2管理yapi服务
    pm2 info yapi //查看服务信息
    pm2 stop yapi //停止服务
    pm2 restart yapi //重启服务
    

升级

cd  {项目目录}
yapi ls //查看版本号列表
yapi update //更新到最新版本
yapi update -v {Version} //更新到指定版本

3、登录yapi,添加分组与项目

4、idea整合yapi插件

安装完插件后,在项目的.idea目录下misc.xml文件添加配置

<component name="yapi">
  <option name="projectToken">08c610184f731d6658f767eb605ffd2eaccf3d6dc1effb875f91beee29a08364</option>
  <option name="projectId">11</option>
  <option name="yapiUrl">http://10.16.151.170:8081</option>
  <option name="projectType">api</option>
</component>

projectToken、projectId需要在自己的yapi平台上配置获取的

可以单个接口上传那就选中方法右键上传即可,批量上传那就选中类名右键上传。

查看API

2、异步任务

@Async注解

@Service
public class AsyncService {

	public void hello(){
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

如果现在调用hello方法,前端需要等待3秒才会返回结果,如果我们使用异步处理,后台开启线程做数据处理,前端就不需要等待了,我们可以在hello方法上加上@Aysnc注解让它成为一个异步方法

@Async // 告诉Spring这是一个异步方法,默认使用线程池,效率很好
public void hello(){
  try {
    TimeUnit.SECONDS.sleep(3);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("数据处理中");
}

还需要开启注解@EnableAsync

@SpringBootApplication
@EnableAsync
public class SpringBootDataStudyXjwApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootDataStudyXjwApplication.class, args);
	}
}

业务方法如果需要回调通知前端,可以结合websocket、juc的CompletableFuture做异步回调

异步失效

如果我们在一个类的方法中调用同一个类的有@Async注解的方法,这种情况下异步不会生效,@Transational也是同理事务控制也不会生效,因为注解的本质的通过动态代理生成代理类取执行方法生效的,原因分析如下:

spring 在扫描bean的时候会扫描该类的方法上是否包含 @Async 注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类 proxy),代理类是继承原来那个bean的,并且重写了父类中被 @Async 注解的方法(如果该注解是加在了类上,则会重写该类的所有方法),并利用AOP切面为这些方法加上异步逻辑。

此时,当这个有注解的方法被调用的时候,实际上调用的是代理类中重写过的方法。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么就不会调用该代理类了,而是直接通过当前对象去调,所以也就不生效了,所以我们看到的现象就是该异步方法没有生效。伪代码:

@Service
class A{
    @Async
    method b(){...}
     
    method a(){    //标记1
        b();
    }
}
//Spring扫描注解后,会创建另外一个代理类,并对添加注解的方法根据切入点创建代理
//调用代理,执行切入点处理器invoke方法,实现异步执行
class proxy$A{
    A objectA = new A();
    method b(){    //标记2
        //MethodInterceptor.invoke
        objectA.b();
    }
  
    method a(){    //标记3
        objectA.a();    //由于a()没有注解,所以不会创建代理,而是直接调用A的实例的a()方法
    }
}

当我们调用A的bean的a()方法的时候,也是被proxy$A拦截,执行proxy$A.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行。

JDK动态代理的核心是代理处理类会拦截所有代理接口的方法,通过处理类中的invoke去调用目标类中的方法,此时就可以在invoke中加入调用目标类方法的前后逻辑,例如记录日志。

参考: https://www.cnblogs.com/sueyyyy/p/14212953.html

使用@Aysnc注解一般都指定自定义的线程池,因为使用spring默认提供的线程池SimpleAsyncTaskExecutor每次都是new创建的,容易出现OOM

参考:https://zhuanlan.zhihu.com/p/432259634

3、定时任务

Timer类

这个是从jdk1.3就开始有的工具类,使用它可以简单的实现定时任务

private static final SimpleDateFormat dateFormat =
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

public static void main(String[] args) throws Exception {
  // 创建定时器
  Timer timer = new Timer();

  // 提交计划任务
  timer.schedule(new TimerTask() {
    @Override
    public void run() {
      System.out.println("定时任务执行了...");
    }
  }, dateFormat.parse("2020-12-08 20:30:00"));
}

@Scheduled注解

cron表达式

语法:秒 分 小时 日 月 周 年[非必填]

周:1-7,1是星期一,7是星期六

在线生成:http://www.bejson.com/othertools/cron/

* 任意时间
? /周冲突匹配
- 区间如6-7
L 最后
W 工作日
# 星期如4#2第二个星期三

常用例子:

0 0/2 * * * ?    表示每2分钟 执行任务 
0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时 
0 15 10 ? * 6L    每月的最后一个星期五上午10:15触发  
0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发

例子

// 定时任务类
@Service
public class ScheduledService {

	// 每分钟执行一次
	@Scheduled(cron = "0 * * * * ?")
	public void hello(){
		System.out.println("Hello ......");
	}
} 

开启注解

@SpringBootApplication
@EnableAsync // 开启异步注解的支持
@EnableScheduling // 开启定时任务的支持
public class SpringBootDataStudyXjwApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootDataStudyXjwApplication.class, args);
	}
}

为什么@Scheduled定时任务是单线程的

从源码入手,从@EnableScheduled这个注解入手,找到ScheduledTaskRegistrar类,其中有一段代码如下:

protected void scheduleTasks() {
  if (this.taskScheduler == null) {
  	this.localExecutor = Executors.newSingleThreadScheduledExecutor();
  	this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
  }
}

如果taskSchedulernull,则创建单线程的线程池:Executors.newSingleThreadScheduledExecutor()

三种方案配置多线程的定时任务:

  1. 直接实现SchedulingConfigurer这个接口,设置taskScheduler,代码如下:

    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
    	@Override
    	public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    		//设定一个长度10的定时任务线程池
    		taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    	}
    }
    
  2. 通过配置开启

    Spring Boot quartz 已经提供了一个配置用来配置线程池的大小,如下

    spring.task.scheduling.pool.size=10
    

    只需要在配置文件中添加如上的配置即可生效

  3. 结合@Async

    @Async这个注解都用过,用来开启异步任务的,使用@Async这个注解之前一定是要先配置线程池

    的,配置如下:

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
      ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
      poolTaskExecutor.setCorePoolSize(4);
      poolTaskExecutor.setMaxPoolSize(6);
      // 设置线程活跃时间(秒)
      poolTaskExecutor.setKeepAliveSeconds(120);
      // 设置队列容量
      poolTaskExecutor.setQueueCapacity(40);
      poolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
      // 等待所有任务结束后再关闭线程池
      poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
      return poolTaskExecutor;
    }
    

    然后在@Scheduled方法上标注@Async这个注解即可实现多线程定时任务,代码如下:

    @Async
    @Scheduled(cron = "0/2 * * * * ? ")
    public void test2() {
    	System.out.println("..................执行test2.................");
    }
    

4、邮件任务

邮件的开发者权限获取

使用qq邮箱需要获取授权码,其他邮箱不用,如163

开启POP3/SMTP服务,得到授权码(相当于qq密码,可以登录你的邮箱),使用其他邮箱也需要开启这个服务获取搜权码

还有pop服务器,smtp服务器的地址,发邮件需要配置

测试

1、导入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2、配置文件

分析源码 MailSenderAutoConfiguration(邮件发送自动配置类),它绑定了配置文件属性类MailProperties,通过导入MailSenderPropertiesConfiguration和MailSenderJndiConfiguration,注入Bean: JavaMailSenderImpl 发送邮件的实现类

application.yaml 或application.properties配置属性,我这里使用网易邮箱,因为qq的密保问题没开启qq邮箱的smtp服务。

# 发件人信息
spring.mail.username=134xxxxx60@163.com
spring.mail.password=你的授权码
spring.mail.host=smtp.163.com

# 使用qq邮箱需要配置ssl安全连接,其他邮箱不用配置
spring.mail.properties.mail.smtp.ssl.enable= true

3、测试使用

@SpringBootTest
class SpringBootApplicationTests {
	@Autowired
	JavaMailSenderImpl javaMailSender;

	@Test
	void contextLoads() throws SQLException {
		// 发送简单邮件,万物皆对象,邮件也不例外
		SimpleMailMessage message = new SimpleMailMessage();
		message.setSubject("通知,飞天班第13节作业"); // 主题
		message.setText("给自己的小破站增加邮件发送功能");
		message.setTo("13435410760@163.com","747463168@qq.com");
		message.setFrom("13435410760@163.com");

		javaMailSender.send(message);
	}
}

执行发现异常

查询官方给出的是:554 DT:SPM 发送的邮件内容包含了未被许可的信息,或被系统识别为垃圾邮件。请检查是否有用户发送病毒或者垃圾邮件

试着修改内容还是发送不出去,在收件人给自己也发送一份才可以。

发送复杂邮件

// 发送复杂邮件,可以结合富文本编辑器开发前端
	@Test
	void sendMail() throws MessagingException {
		MimeMessage message = javaMailSender.createMimeMessage();
		// 复杂邮件,通过辅助类完成
		MimeMessageHelper helper = new MimeMessageHelper(message,true);
		// 基本信息
		helper.setSubject("复杂邮件");  // 主题
		helper.setText("<blockquote style=\"text-align: center;margin-bottom: 10px;\n" +
				"padding: 15px;\n" +
				"line-height: 22px;\n" +
				"border-left: 5px solid #009688;\n" +
				"border-radius: 0 2px 2px 0;\n" +
				"background-color: #f2f2f2;\">\n" +
				"layui 兼容人类正在使用的全部浏览器(IE6/7除外),可作为 PC 端后台系统与前台界面的速成开发方案。\n" +
				"</blockquote>",true);    // html文本
		// 发送附件
		helper.addAttachment("layui.png",new File("/Users/xjw/Pictures/layui.png"));

		// 发送对象
		String[] receiver = {"13435410760@163.com","747463168@qq.com"};
		helper.setTo(receiver);
		helper.setFrom("13435410760@163.com");

		javaMailSender.send(message);
	}

5、富文本编辑器Editormd

注意bug:maven过滤资源的时候,可能会损坏一些文件,如图标

<build>
   <!-- 注意:mybatis的xml文件需要导出!
            问题:java目录导出没有问题,resources目录过滤也没有问题
         -->
   <resources>
     <resource>
       <directory>src/main/java</directory>
       <includes>
         <include>**/*.xml</include>
       </includes>
       <filtering>true</filtering>
     </resource>
     <!-- maven过滤资源的时候,可能会损坏一些文件,如图标! -->
     <!--<resource>-->
     <!--<directory>src/main/resources</directory>-->
     <!--<includes>-->
     <!--<include>**/*.xml</include>-->
     <!--</includes>-->
     <!--<filtering>true</filtering>-->
     <!--</resource>-->
   </resources>
   <plugins>
     <plugin>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-maven-plugin</artifactId>
     </plugin>
   </plugins>
</build>

editormd是一个开源的markdown编辑器,官网 http://editor.md.ipandao.com/

文章表

可扩展字段:标签,时间,浏览量,点赞,评论表

编写一个editor.html页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>Jude'Blog</title>
	<meta name="renderer" content="webkit">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<meta name="viewport" content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" />

	<!--editormd-->
	<link rel="stylesheet" th:href="@{/editormd/css/editormd.css}">
	<link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" />
</head>
<body>
<div class="layui-fluid">
	<div class="layui-row layui-col-space15">
		<div class="layui-col-md12">
			<!--博客表单-->
			<form name="mdEditorForm">
				<div>
					标题: <input type="text" name="title">
				</div>
				<div>
					作者: <input type="text" name="author">
				</div>
				<!-- 文章的主体内容 textarea -->
				<div id="article-content">
					<textarea name="content" id="content" style="display:none;"> </textarea>
				</div>
			</form>
		</div>
	</div>
</div>
</body>
<!--editormd-->
<script th:src="@{/editormd/jquery.min.js}"></script>
<script th:src="@{/editormd/editormd.js}"></script>

<script>
	var testEditor;
	$(function () {
		testEditor = editormd("article-content",{
		    width: "95%",
			height: 500,
			syncScrolling: "single",
			path: "/editormd/lib/", // 依赖模块路径
			saveHTMLToTextarea: true,   // 保存html到textarea
			emoji: true,  // 开启表情的功能,图片本地配置
			// markdown的配置
			tex: true, // 开启科学公式tex语言支持
			flowChart:  true,   //开启流程图支持
			// 图片上传
			imageUpload: true,
			imageFormats: ["jpg","jpeg","gif","png"],
			imageUploadURL: "/demo/article/file/upload", // 文件上传请求
			onload: function () {
				console.log('onload',this);
      },
      /*指定需要显示的功能按钮*/
      toolbarIcons : function() {
        return ["undo","redo","|",
                "bold","del","italic","quote","ucwords","uppercase","lowercase","|",
                // "h1","h2","h3","h4","h5","h6","|",
                "list-ul","list-ol","hr","|",
                "link","reference-link","image","code","preformatted-text",
                "code-block","table","datetime","emoji","html-entities","pagebreak","|",
                "goto-line","watch","preview","fullscreen","clear","search","|",
                //"help","info",
                "releaseIcon", "index"]
      },
			// 自定义功能按钮,一个发布,一个返回首页
			toolbarIconTexts:{
        releaseIcon: '<span bgcolor="gray">发布</span>',
				index: '<span bgcolor="red">返回首页</span>'
			},
			// 给自定义按钮指定回调函数
      /**
      * @param {Object}      cm         CodeMirror对象
      * @param {Object}      icon       图标按钮jQuery元素对象
      * @param {Object}      cursor     CodeMirror的光标对象,可获取光标所在行和位置
      * @param {String}      selection  编辑器选中的文本
      */
			toolbarHandlers:{
        releaseIcon: function (cm,icon,cursor,selection) {
          // 表单提交
          mdEditorForm.method = "post";
          mdEditorForm.action = "/article/addArticle"; // 提交到服务器
          mdEditorForm.submit();
        },
        index: function () {
          window.loaction.href = "/";
        }
			}
		})
  })
</script>
</html>

editormd的插件:

本地emoji表情

我们把本地表情图片放到emoji-dialog/emoji下。打开editormd.js,里面 editormd.defaults 就是默认的配置参数,创建editormd实例自己可以参考修改。找到editormd.emoji 把path修改为本地表情路径

editormd.emoji = {
  path  : "/editormd/plugins/emoji-dialog/emoji/",
  ext   : ".png"
};

文件上传与静态访问

图片本地上传的请求路径是/demo/article/file/upload,后台contorller写上对应的请求方法,json的返回格式:

// 给editormd回调
JSONObject res = new JSONObject();
res.put("url","/upload/"+month+"/"+filename);
res.put("success",1);
res.put("message","upload success");

配置upload路由静态资源访问

@Configuration
public class ResourceConfig implements WebMvcConfigurer {

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		// 文件保存到当前项目的upload目录下
		// 访问的时候使用/upload/1.png就OK了
		registry.addResourceHandler("/upload/**").addResourceLocations("file:"+System.getProperty("user.dir")+"/upload/");;
	}
}

这样本地上传图片就可以了。

6、单体应用知识点总结

作业

给自己的博客网站,增加富文本编辑器的邮件发送功能

Post Directory