Lesson41_SpringBoot

SpringBoot

简介及结构

基本概念

什么是springboot
  • Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。
  • 个人理解,spring boot不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架
为什么使用springboot
平时如果我们需要搭建一个spring web项目的时候需要怎么做呢?
  • 配置web.xml,加载spring和spring mvc
  • 配置数据库连接、配置spring事务
  • 配置加载配置文件的读取,开启注解
  • 配置日志文件
  • 其他一些配置
  • 配置完成之后部署tomcat 调试
微服务
  • 现在非常流行微服务,如果我这个项目仅仅只是需要发送一个邮件,如果我的项目仅仅是生产一个积分;我都需要这样折腾一遍!
  • 但是如果使用spring boot呢?很简单,我仅仅只需要非常少的几个配置就可以迅速方便的搭建起来一套web项目或者是构建一个微服务
springboot优点
  • 轻松创建独立的Spring应用程序。
  • 内嵌Tomcat、jetty等web容器,不需要部署WAR文件。
  • 提供一系列的“starter” 来简化的Maven配置,集成主流开源产品往往只需要简单的配置即可。
  • 开箱即用,尽可能自动配置Spring。
  • 使用sping boot非常适合构建微服务

springboot项目

springboot项目构建
方式一(在线构建):

会下载到本地,以后要用的时候,直接解压就可以用了,不用每次去下载

  • 访问http://start.spring.io/
  • 选择构建工具Maven Project、Spring Boot版本以及一些工程基本信息,点击“Switch to the full version.”java版本选择1.8,具体如下:
    在这里插入图片描述
  • 点击Generate Project下载项目压缩包
  • 下载后解压
  1. 使用eclipse,Import -> Existing Maven Projects -> Next ->选择解压后的文件夹-> Finsh,OK done!
  2. 使用IDEA,直接打开项目,OK
  • 最后,右键启动运行Application main方法,至此一个java项目搭建好了
方式二(开发工具):

实际上也是借助了http://start.spring.io/
在这里插入图片描述

方式三(maven构建):
  • 根据需要创建一个maven项目
  • 导入springboot需要的依赖
  • 创建包,创建启动类
  • 启动项目
springboot项目结构

Spring Boot的基础结构共三个文件:

  • src/main/java 程序开发以及主程序入口
  • src/main/resources 配置文件
  • src/test/java 测试程序

spingboot建议的目录结果如下:

  • 根目录结构:com.example.myproject (myproject对应项目的名字)
    在这里插入图片描述
  • Application.java(DemoApplication) 建议放到跟目录下面,主要用于做一些框架配置
  • domain目录主要用于实体(Entity)与数据访问层(Repository)
  • service 层主要是业务类代码
  • controller 负责页面访问控制

resources ⽬录下:

  • static ⽬录存放 web 访问的静态资源,如 js、css、图⽚等;
  • templates ⽬录存放⻚⾯模板;
  • application.properties 存放项⽬的配置信息。

采用默认配置可以省去很多配置,当然也可以根据自己的喜欢来进行更改
@SpringBootApplication
是一个组合注解,用于快捷配置启动类
该注解等价于同时使用3个注解@EnableAutoConfiguration+@ComponentScan+@Configuration
默认值

搭建SpringMVC环境
第一步:在pom文件中添加web依赖
  • 项目创建好后springboot相关的版本已经确定
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.6</version>
    <relativePath/> <!-- lookup parent from repository -->
 </parent>
  • 直接导入web依赖,默认不需要版本信息
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 可以在创建项目时就确定该依赖

pom.xml文件中默认有两个模块:

  • spring-boot-starter:核心模块,包括自动配置支持、日志和YAML;
  • spring-boot-starter-test:测试模块,包括JUnit、Hamcrest、Mockito。
第二步:编写controller内容
  • 使用SSM的方式创建controller类
    @Controller
    public class Demo01Controller {
    	@ResponseBody
    	@RequestMapping("/demo01")
    	public String demo01() {
        	return "我是按照ssm方式的controller";
    	}
    }
    
  • 使用springboot提供的注解创建
    @RestController
    public class Demo02Controller {
    	@RequestMapping("/demo02")
    	public String demo02() {
        	return "使用spring boot 提供的注解创建的controller";
    	}
    }
    
  • @RestController 注解相当于 @ResponseBody + @Controller 合在⼀起的作⽤,如果 Web 层的类上使⽤了
  • @RestController 注解,就代表这个类中所有的⽅法都会以 JSON 的形式返回结果,也相当于JSON 的⼀种快捷使⽤⽅式;
  • @RestController的意思就是controller里面的方法都以json格式输出,不用再写什么jackjson配置的了!也就是不需要在方法上面加@ResponseBody注解了
  • 需要注意的是,无论里面的方法返回值是什么类型,都会自动转为json格式。
第三步:右键启动Application.java(DemoApplication) 主程序
  • 打开浏览器访问http://localhost:8080/hello(注意不要工程名字),就可以看到效果了
  • 使用SSM方式的
    在这里插入图片描述
  • 使用springboot方式的
    在这里插入图片描述
  • 注意

(spring boot启动主程序,会扫描启动类当前包和以下的包(一般是直接在跟目录下面),如将 spring boot启动类放在包 com.dai.controller 里面的话 ,它会扫描 com.dai.controller 和 com.dai.controller.* 里面的所有的,如果是要扫描其他包,这有一种解决方案是,在启动类的上面添加 @ComponentScan(basePackages={“com.dai.controller”})
@ComponentScan就是扫描有@Controller,@Service,@Repository,@Component注解的类,但是这里注意,不要去扫描dao层的Mapper接口,否则分页插件后面不能用

热部署
  • 添加热部署依赖插件
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
    
  • 设置springbootmaven的插件属性
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <fork>true</fork>
        </configuration>
     </plugin>
    
  • 此时如果还不行,打开idea自动编译设置
    在这里插入图片描述
  • 此时大部分可以,如果还是不行,可以通过ctrl+alt+shift+/,打开会话,选择registry,找到自动编译,打勾
    • 该模块在完整的打包环境下运行的时候会被禁用
      在这里插入图片描述

资源属性配置

springboot默认资源属性文件

概念
  • springboot项目构建后,在resources下有一个application.properties文件,就是springboot默认的资源配置文件
  • 该文件默认为空。
内容包含
  • 可以包含springboot的设置
  • 与其他框架整合的配置
  • 自己自定义一些属性
  • 自定义上传文件路径,上传文件大小等属性的设置
  • 内容设置是键值对的形式
  • 键可以是单个的键名,比如name,age等
  • 也可以带有前缀,比如user.name,jdbc.driver等
使用(模拟在controller中使用配置中的值)
第一步:根目录下增加任意类
  • 这个类的作用就是将配置文件中的键值对映射到类属性中,然后通过类的对象获取值
  • 此处我们在根目录下先增加一个包,properties
  • 再在包中增加一个类,这个类我们称为属性文件处理类
    在这里插入图片描述
  • 类中属性名称与配置文件中键名一致,就会自动映射
  • 为属性提供该有的getter和setter方法
    /**
    * @author: 邪灵
    * @date: 2021/11/5 20:54
    * @version: 1.0
    */
    @Component
    @ConfigurationProperties(prefix = "jdbc")
    public class ApplicationProperties {
    	private int id;// 将application.properties里面的jabc.id注入给当前属性
    	private String name;
    	private String pass;
    
    	public int getId() {
        	return id;
    	}
    
    	public void setId(int id) {
        	this.id = id;
    	}
    
    	public String getName() {
        	return name;
    	}
    
    	public void setName(String name) {
        	this.name = name;
    	}
    
    	public String getPass() {
        	return pass;
    	}
    
    	public void setPass(String pass) {
        	this.pass = pass;
    	}
    }
    
  • 注意

    @Component注解是将该处理类交于spring容器管理,这样该类与配置文件都会同时被spring容器加载
    @ConfigurationProperties(prefix = “jdbc”)注解会自动加载根目录下的叫做application.properties的配置文件(默认资源属性文件)

    会根据prefix指定的键名前缀,加上属性名称,匹配配置文件中的键名
    找到匹配的键名,会将值赋值给对应键名的属性

    之后我们从spring容器中获取该处理类对象时,其属性值就是从配置中获取的值

第二步
  • 在默认属性配置文件中增加属性
  • 配置文件中键名前缀与获取时一致
  • 配置文件中键名与处理类中属性名一致
  • 注意有些关键字不能使用,比如user.name会获取到当前电脑系统用户名
    jdbc.id=10
    jdbc.name=你好
    jdbc.pass=nihao
    
第三步
  • 在controller中注入处理类对象
  • 注入对象时可以用@Autowired自动注入或者@Autowired跟@Qualifier组合注入
  • 也可以使用java提供的@Resource,建议使用该注解
  • 获取对象属性查看是否成功获取到配置文件中的值
    @Controller
    public class Demo01Controller {
    	@Autowired
    	private ApplicationProperties applicationProperties;
    
    	@ResponseBody
    	@RequestMapping("/demo01")
    	public String demo01() {
        	return "我是"+applicationProperties.getId()+"我叫"+applicationProperties.getName();
    	}
    }
    
  • 可以看出配置中的值已经被映射到对象属性中了(虽然出现了乱码)
    在这里插入图片描述

自定义属性配置

SpringBoot自定义配置文件不强制使用application.propertis,可以自定义一个名为my2.properties的资源文件,命名不强制application开头
一般用application作为文件名后缀,比如myApplication.properties等

注意点
  • 同一项目中,如果有自定义属性文件和默认属性文件,两个文件中的属性前缀尽量不要出现重复。可能会报错
  • 无论自定义属性文件或默认属性文件,键值对键名前缀一般是一个单词,使用多单词驼峰命名是@ConfigurationProperties注解指定前缀会报错
  • 自定义属性文件的加载与默认属性文件的加载几乎一样,唯一不同的是在映射类中要指定加载的配置文件地址
使用
第一步:在application.properties属性文件相同目录下面增加自定义属性文件(要求与默认属性文件同目录)

在这里插入图片描述

第二步:增加映射类,映射自定义属性文件中的内容(位置与默认属性映射类一样,随意即可)
  • 注意事项

    增加@Component,用来将映射类交于spring容器加载管理,与前面默认属性文件加载一样
    增加@PropertySource(“classpath:myApplication.properties”),用来指定需要加载的属性配置文件地址
    增加@ConfigurationProperties(prefix = “myapp”),用来加载配置文件,指定内容键值对键名前缀,与前面默认属性文件加载一样

  • 代码示例
@Component
@PropertySource("classpath:myApplication.properties")
@ConfigurationProperties(prefix = "myapp")
public class MyApp {
    private int id;
    private String name;
    private String pass;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPass() {
        return pass;
    }

    public void setPass(String pass) {
        this.pass = pass;
    }
}
第三步:在controller中测试使用
@RestController
public class Demo02Controller {
    @Resource
    private MyApp myApp;
    @RequestMapping("/demo02")
    public String demo02() {
        return "自定义属性文件:"+myApp.getId()+myApp.getName()+myApp.getPass();
    }
}

多环境化属性配置

概念

在真实的应用中,常常会有多个环境(如:开发,测试,生产等),不同的环境数据库连接都不一样,这个时候就需要用到spring.profile.active的强大功能了,它的格式为application-{profile}.properties,这里的application为前缀不能改,{profile}是我们自己定义的

  • spring.profile.active:就是springboot内置的属性,直接在默认属性文件application.properties中指定
  • application-{profile}.properties:多环境多个配置文件,{profile}自定义,不同的环境中不同的文件名
  • spring.profile.active后面的值就是指定自定义的{profile}文件名
  • 项目启动后默认加载默认的属性配置文件,然后运行到spring.profile.active再加载指定的配置文件,然后映射出内容
  • 一般多个多环境文件中的属性都是一致的。只是值不同,这样多个文件共用一个映射类,只是根据默认属性文件中spring.profile.active的值的不同,加载的内容不同。映射到的值不同。
使用
第一步:创建多个属性配置文件,模拟多环境文件
  • application-dev.properties
    server.servlet.context-path=/dev
    me.name=dev
    me.pass=123
    
  • application-test.properties
    server.servlet.context-path=/test
    me.name=test
    me.pass=789
    
  • application-prod.properties
    server.servlet.context-path=/prod
    me.name=prod
    me.pass=456
    
  • 注意点

    1.server.servlet.context-path=/prod

    该属性的值前面会后斜杠,如果不写,启动会报错
    该属性是设置http访问的项目名称,可以在任意配置文件中‘
    如果没有设置该属性,默认访问地址是没有项目名称的。
    那么项目启动后浏览器访问就是通过http://localhost:8080,后面跟上要访问的地址,比如controller的RequestMapping地址
    加上该属性后,访问地址的项目名称就是指定的名称,比如目前访问controller的地址为:
    http://localhost:8080/prod/后面跟上要访问的地址,比如controller的RequestMapping地址

    2.当只有默认属性配置文件时,在默认属性文件中指定了server.servlet.context-path=/prod,访问地址就是该地址
    3.如果在默认属性配置和其他属性配置文件中都指定了该属性,那么当使用spring.profile.active指定了要加载的属性配置文件时,指定属性文件中的项目名会覆盖默认属性文件中的项目名

第二步:在默认属性配置文件中指定需要加载的其他配置文件名称
server.servlet.context-path=/abc
# 此时就是指定要加载的配置文件为application-test.properties文件
spring.profiles.active=test

这里test实际就是要等于前面属性文件名字里面的{profile}值,我这里还可以是dev、prod,(表示当加载默认的application.properties属性文件的时候,会自动的去加载对应的application-test.properties属性文件)
spring.profiles.active=test

第三步:增加属性文件映射类
@Component
@ConfigurationProperties(prefix = "me")
public class DevProdTest {
    private String name;
    private String pass;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPass() {
        return pass;
    }

    public void setPass(String pass) {
        this.pass = pass;
    }
}

不需要指定加载配置文件的位置,加载默认配置文件即可
指定加载的前缀就是多环境配置文件中属性的前缀,其他与前面一直

在controller中测试
@RestController
public class Demo03Controller {
    @Resource
    private DevProdTest devProdTest;

    @RequestMapping("/demo")
    public String demo01() {
        return "多环境:"+devProdTest.getName()+devProdTest.getPass();
    }
}

这个时候我们再次访问http://localhost:8080/demo就没用处了,会访问不到
因为我们在默认配置中增加了server.servlet.context-path=/abc属性,访问地址就为:http://localhost:8080/abc/demo
又因为我们application.properties里面的spring.profiles.active的值,激活了对应的配置文件,读取到里面的值了
而激活的配置文件中我们也设置了server.servlet.context-path=/bcd,该值覆盖了默认文件中的值
所以最终访问地址为http://localhost:8080/bcd/demo

注意:
我们修改application.properties里面的spring.profiles.active的值,可以看出来我们激活不同的配置读取的属性值是不一样的,注意,只能访问spring.profiles.active对应值的工程

配置视图

官方不推荐jsp的支持(jar包不支持jsp,jsp需要运行在servletContext中,war包需要运行在server服务器中如tomcat)
官方推荐使用thymeleaf,freemarker等模版引擎

springboot配置jsp页面跳转

第一步:加入依赖
<!--springboot对jsp页面的依赖-->
<dependency>
   <groupId>org.apache.tomcat.embed</groupId>
   <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--jsp页面中jstl表达式的支持-->
<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>jstl</artifactId>
</dependency>
第二步:创建webapp及子目录
  • 在根目录(main)下创建普通目录即可(springboot不支持jsp,无法直接创建web模块)
    在这里插入图片描述
  • 需要注意

    springboot不支持jsp,故无法直接创建web模块,需要手动创建普通目录
    创建好webapp目录后在下面创建view目录用于放置jsp页面,或者创建WEB-INF放置jsp都可以

  • 访问时(webapp作为根目录,不需要出现在url中

    http://localhost:8080/a.jsp 可以访问
    http://localhost:8080/view/c.jsp 可以访问
    http://localhost:8080/WEB-INF/b.jsp 不能访问

  • 注意:如果是直接访问jsp,jsp放置于view下,可以直接进行访问,如下:
    在这里插入图片描述
  • 可以看到如果是浏览器直接访问,输入地址即可,与之前web或SpringMVC一样,不需要其他配置
第三步:增加配置(如果是controller访问,需要配置视图,类似SpringMVC试图解析)
  • 在默认配置文件中配置视图前缀和后缀
    #jsp访问配置
    server.servlet.context-path=/abc
    spring.profiles.active=test
    spring.mvc.view.prefix=/view/
    spring.mvc.view.suffix=.jsp
    
第四步:测试运行(用法与SpringMVC一致)
  • 此时需要跳转的是jsp页面,controller中返回值是视图名称
  • 创建普通的controller,不能使用RestController注解,次注解返回的都是json字符串
  • 使用普通的controller,返回字符串
    // 此处如果使用@RestController注解,返回的都是json,无法跳转到jsp
    @Controller
    public class ViewTest {
    	@RequestMapping("/index01")
    	public String index() {
        	return "index";
        }
    }
    
    在这里插入图片描述
  • 使用普通controller,返回ModelAndView,附带参数也可
    @RequestMapping("/index02")
    public ModelAndView index02() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("index");
        mav.addObject("user","USER");
        return mav;
    }
    
    jsp中接收参数
    <h1>我是index页面</h1>
    <h2>mav:${user}</h2>
    
    页面正常访问,参数正常获取
    在这里插入图片描述

springboot配置Thymeleaf模板

第一步:导入依赖,把前面jsp的删除

一般一个项目中不糊同时出现thymeleaf和jsp,所以导入thymeleaf依赖时先删除jsp的依赖

<!--Thymeleaf依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
第二步:application.properties 中添加配置,把前面jsp的删除
默认情况(文件名固定)
  • 默认的thymeleaf的HTML页面存放位置在resources下的
  • 此时默认的前缀和后缀,一般idea会有提示的
  • 而如果是默认的位置,因为值也是默认的,可以不写
  • 如果要写,就是下面的值,不能更改
    # thymeleaf视图解析
    # 先禁用thymeleaf缓存,一般开发或测试环境都是禁用的
    spring.thymeleaf.cache=false
    # 默认的解析前缀地址为resources下的templates下,所以可以不写前缀
    spring.thymeleaf.prefix=classpath:/templates/
    # 默认的解析后缀为.html,所以可以不写后缀
    spring.thymeleaf.suffix=.html
    
自定义位置(文件名自定义)
  • html页面的位置自定义,一般就是与前面jsp一样,也就是mvc的做法,新建webapp,然后新建目录存放
  • 此时前缀配置的值与jsp时无异
  • 而后缀因为是默认的.html,所以可以不写
    # thymeleaf视图解析
    # 先禁用thymeleaf缓存,一般开发或测试环境都是禁用的
    spring.thymeleaf.cache=false
    # 默认的解析前缀地址为resources下的templates下,所以可以不写前缀
    # spring.thymeleaf.prefix=classpath:/templates/
    # 默认的解析后缀为.html,所以可以不写后缀
    # spring.thymeleaf.suffix=.html
    
    # 自定义位置
    spring.thymeleaf.prefix=/temp/
    # 后缀可以写,可以不写,默认就是.html
    
第三步:webapp/temp或者resources/templates下面新建HTML页面
  • HTML页面是thymeleaf格式的
    <html xmlns:th="http://www.thymeleaf.org">
    <body>
    aa.html是的<h1 th:text="${a}">Hello World  作用域必须有a,否则出错</h1>
    </body>
    
  • 页面的位置如下
    在这里插入图片描述
第四步
  • 直接访问html 可以访问是webapp下面的,与之前jsp类似
  • 访问controller,返回ModelAndView
    @Controller
    public class ThymeleafTest {
    
    	@RequestMapping("/thy01")
    	public ModelAndView thy01() {
        	ModelAndView mav = new ModelAndView();
        	mav.setViewName("index");
        	mav.addObject("user","USER");
        	return mav;
    	}
    }
    
  • 访问controller,返回String,也可以正常访问index.html
    @RequestMapping("/thy02")
    public String thy02() {
        return "index";
    }
    

springboot整合Mybatis

springboot整合mybatis

第一步:导入依赖
导入springboot整合mybatis的依赖

此处与SSM不同,SSM中需要导入mybatis依赖,需要导入spring整合mybatis依赖,此处只需要导入一个springboot整合依赖即可
springboot整合mybatis依赖中将相关的依赖都进行了整合,只需要导入这一个依赖就可以了

<!--springboot整合mybatis依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.3</version>
</dependency>
导入数据库mysql驱动依赖

此处需要注意的是,导入依赖时不需要指定版本信息
如果默认的版本不能 使用,可以自行确定版本信息

<!--数据库驱动依赖-->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
</dependency>
第二步:配置文件application.properties中添加配置
添加数据源配置信息

数据源信息相关属性为springboot内置,idea直接有提示

spring.datasource.url=jdbc:mysql://localhost:3306/xdz2104
spring.datasource.username=root
spring.datasource.password=root
  • 注意

    1.此处因为springboot默认的数据库驱动版本,不需要指定driver
    2.需要注意的是默认的数据库驱动版本为5.3版本的

添加mybatis配置信息

mybatis配置信息属性为整合依赖中提供
在导入springboot整合mybatis依赖后作用到项目中后才会有提示

# 指定mapper.xml文件的地址
mybatis.mapper-locations=classpath:mapper/*.xml
# 开启别名扫描包
mybatis.type-aliases-package=com.example.demo.entity
# 开启驼峰命名,数据库匈牙利命名与java驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true
  • 注意

    1.只需要配置扫描mapper的包即可,一般mapper.xml文件都是放在resources下的
    2.在resources下新建一个文件夹(mapper目录),存放所有的mapper.xml文件

第三步:创建表,实体类,dao接口及mapper文件等
创建实体类与数据库表
public class Users {
    private Integer id;
    private String name;
    private String pass;
    private int age;
    private String sex;

    public Users() {
    }

    public Users(String name, String pass, int age, String sex) {
        this.name = name;
        this.pass = pass;
        this.age = age;
        this.sex = sex;
    }

    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 String getPass() {
        return pass;
    }

    public void setPass(String pass) {
        this.pass = pass;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}
创建mapper接口
  • 方式一:在所有的接口上添加@Mapper,否则springboot无法扫描mapper接口
    @Mapper
    public interface UserDao {
    	Users queryById(int id);
    	List<Users> queryAll();
    }
    
  • 方式二:不需要在接口上添加mapper,在启动类上添加mapper接口扫描包注解
    @SpringBootApplication
    @MapperScan("com.example.demo.dao")
    public class DemoApplication {
    
    	public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
    	}
    
    }
    
  • 注意:

    要么在所有的接口上添加@Mapper注解,保证springboot扫描mapper接口
    @Mapper注解是org.apache.ibatis.annotations.Mapper;提供的。要注意,后续有不同包的同名注解用到,对应后面的最后一条说明,
    如果接口较多,挨个添加麻烦,可以在启动类上添加@MapperScan(“com.example.demo.dao”),指定mapper所在的包,让springboot进行扫描
    如果springboot没有对接口进行扫描,会报错,接口无法使用,实际是与mapper.xml对应不上,使用时找不到接口的实现类异常
    以上都是没使用通用接口的时候,使用通用接口之后导的包不一样了,@Mapper注解的接口就不同

创建mapper.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.example.demo.dao.UserDao">
    <select id="queryById" resultType="users">
        select * from users where id = #{id}
    </select>
    <select id="queryAll" resultType="users">
        select * from users
    </select>
</mapper>
创建service接口与实现类
  • service接口
    public interface UserService {
    	Users queryById(int id);
    	List<Users> queryAll();
    }
    
  • service实现类
    @Service
    public class UserServiceImpl implements UserService {
    	@Resource
    	private UserDao userDao;
    	@Override
    	public Users queryById(int id) {
        	return userDao.queryById(id);
    	}
    
    	@Override
    	public List<Users> queryAll() {
        	return userDao.queryAll();
    	}
    }
    
第四步:在controller中测试,通过浏览器访问
  • controller
    @RestController
    public class UserController {
    	@Resource
    	private UserService userService;
    
    	@RequestMapping("/getUser")
    	public List<Users> getUserById() {
        	return userService.queryAll();
    	}
    }
    
  • 访问结果
    在这里插入图片描述
问题
  • 此处springboot默认的数据库驱动为5.3版本的
  • 底层使用的数据库为8.0版本的
  • url后面也没有对时区进行设定
  • 在SSM中,一般驱动版本不能低于数据库版本且8.0数据库时区需要手动设置,否则报错,此处都没有

springboot整合mybatis通用接口及分页插件

导入依赖
导入基础依赖
  • 导入springboot对web的依赖
  • 导入mybatis与springboot整合依赖
  • 导入数据库驱动依赖
导入分页插件依赖
<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>5.2.1</version>
</dependency>
导入分页插件springboot自动配置依赖
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-autoconfigure</artifactId>
	<version>1.2.10</version>
</dependency>
导入分页插件springboot整合依赖
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.2.10</version>
</dependency>
导入通用mapper依赖
<dependency>
	<groupId>tk.mybatis</groupId>
	<artifactId>mapper-spring-boot-starter</artifactId>
	<version>2.1.5</version>
</dependency>
增加配置
增加基础配置
# 指定访问时项目名称
server.servlet.context-path=/abc
# 指定需要激活的配置文件
spring.profiles.active=test
增加springboot数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/xdz2001
spring.datasource.username=root
spring.datasource.password=root
增加mybatis基础配置
# 指定mapper.xml文件的地址
mybatis.mapper-locations=classpath:mapper/*.xml
# 开启别名扫描包
mybatis.type-aliases-package=com.example.demo.entity
# 开启驼峰命名,数据库匈牙利命名与java驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true
增加mybatis执行日志配置
# 输出mybatis执行的SQL语句,前面logging.levvel是固定的
# 后面com.example.demo.dao是指定mybatis执行的dao接口包
# DEBUG指的是打印调试相关的东西,包含SQL语句等
logging.level.com.example.demo.dao=DEBUG
增加通用mapper配置
########## 通用Mapper ##########
# 主键自增回写方法,默认值MYSQL,详细说明请看文档(指定主键自增策略)
mapper.identity=MYSQL
# 指定通用mapper需要继承的类
mapper.mappers=tk.mybatis.mapper.common.BaseMapper
# 设置 insert 和 update 中,是否判断字符串类型!='',也就是参数判空
mapper.not-empty=true
# 枚举按简单类型处理(枚举按简单类型处理,如果有枚举字段则需要加上该配置才会做映射)
mapper.enum-as-simple-type=true
增加分页插件配置
########## 分页插件 ##########
# 指定分页语句(类似方言)
pagehelper.helperDialect=mysql
pagehelper.params=count=countSql
#pagehelper.reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。
pagehelper.reasonable=false
#pagehelper.support-methods-arguments:支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。
pagehelper.supportMethodsArguments=true
创建dao接口及xml映射
创建接口

创建user表,使用Mybatis插件生成pojo对象和接口及xml映射(注意与前面配置的路径一致)
如果是自己写。也不需要定义方法,如果默认的方法不够,再自己添加即可
与之前一致,需要在接口上面添加@mapper注解
如果不想在接口上面添加主键,可以在启动类上添加@MapperScan注解

需要注意的是此处注解的类@MapperScan是tk包下的,不是org下的
两个包下的@MapperScan注解不能同时存在,会报错
如果是没有继承同样mapper的接口,在上面添加@Mapper注解即可

@Mapper //import org.apache.ibatis.annotations.Mapper;
public interface UserDao extends BaseMapper<User> {//Mapper接口,注意必须要有@Mapper
//默认方法删掉
}
创建对应映射文件

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.example.demo.dao.UserDao">
</mapper>
指定主键

如果使用通用Mapper,那么默认的方法增、删、改、查等大多都是根据主键操作
如果不指定主键,那么使用时会键表中所有字段当做主键使用
比如正常主键是id,那么查询应该是select * from user where id = ?
如果没有指定主键,那么同样的通过主键查询语句就是select * from user where id = ? and name = ? and pass = ?
也就是查询时会将所有字段当做主键
要指定主键只需要在实体类中对应属性上面添加@ID注解接口,该注解是javax包下的注解

import javax.persistence.Id;

/**
 * @author: 邪灵
 * @date: 2021/11/9 18:49
 * @version: 1.0
 */
public class Users {
    @Id
    private Integer id;
    private String name;
    private String pass;
    private int age;
    private String sex;
}
测试运行
正常调用service测试
  • 代码
    @RequestMapping("/hello3")
    	public List<User> index3() {
    		List<User> list = userService.selectAll();
    		System.out.println("ss");
    		return list;
    	}
    
  • 控制台SQL显示
: ==> Preparing: SELECT id, name, address,sex,love,imgpath FROM user2
: ==> Parameters:
: <==Total : 257
使用分页测试
  • 使用分页插件只需要在controller中调用service方法前设置即可
  • 设置时需要给定两个参数,页数和每页显示条数
  • 设定后mybatis会使用拦截器的方式,修改原来的SQL语句,添加limit语句

代码

@RestController
public class Hello2 {
	// 依赖service层接口
	@Resource(name = "userServiceImp")
	UserService userService;

	@RequestMapping("/hello3")
	public PageInfo index3() {
		// service方法调用前设定当前也和每页条数
		PageHelper.startPage(2, 5);
		//  正常调用service方法即可,任何方法都可以
		List<User> list = userService.selectAll();
		PageInfo<User> userPageInfo = new PageInfo<>(list);
		System.out.println(list);
		return userPageInfo;
	}
}

控制台SQL语句

: ==>Preparing: SELECT count(0) FROM user2
:==> Parameters:
: <==Total: 1
:==> Preparing: SELECT id, name, address,sex, love,imgpath FROM user2 LINIT 5
: ==> Parameters:
: <==  Total: 5

事务

在springboot中 配置事务需要两步操作

第一步:在启动类中添加@EnableTransactionManagement

在启动类中添加注解,启动事务管理

@SpringBootApplication
@MapperScan("com.example.demo.dao")
@EnableTransactionManagement
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
第二步:在service中需要事务管理的方法上添加@Transactional

在具体需要事务 管理的方法上添加事务,该方法将是一个事务
注解中可以有参数指定,比如指定回滚
还可以指定该方法为只读方法等
当注解没有参数时,可以带有括号也可以没有,都是可以的
该注解还可以用在类上面,该类中所有方法都被事务管理

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserDao userDao;


    @Override
    // 可以指定参数,比如只读方法
    @Transactional(readOnly = true)
    public Users queryById(int id) {
        return userDao.selectByPrimaryKey(id);
    }

    @Transactional
    @Override
    public List<Users> queryAll() {
        return userDao.selectAll();
    }
}
注意点
  • 在使用@Transactional注解时,虽然没有强制需要属性
  • 但是会提示要求添加RollBackFor属性的值
  • 很可能会遇到下面的问题

    1.已注解了@Transactional的事务仍会有“出现异常事务不回滚”的情况?例如mybatis的xml配置标签错误时,运行报异常,但仍然能够进行增加操作。
    2.Java阿里巴巴规范提示,事务需要进行手动回滚。为什么?

  • 出现上面问题的原因如下

    Spring框架的事务管理默认地只在发生不受控异常(RuntimeException和Error)时才进行事务回滚。也就是说,当事务方法抛出受控异常(Exception中除了RuntimeException及其子类以外的)时不会进行事务回滚。

  • 此时rollbackFor属性在这里就可以发挥它的作用了!
  • 在类或方法前注解配置@Transactional(rollbackFor=Exception.class)就可以实现:
  • 当发生受控异常(checked exceptions)时,事务也进行回滚。
@Service
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
    @Resource
    private UserDao userDao;


    @Override
    public Users queryById(int id) {
        return userDao.selectByPrimaryKey(id);
    }
    
    @Override
    public List<Users> queryAll() {
        return userDao.selectAll();
    }
}

文件上传

普通文件上传

环境配置
添加web依赖
# web依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
thymeleaf上传(添加thymeleaf依赖)
# thymeleaf依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
	<version>2.0.4.RELEASE</version>
</dependency>
jsp上传(添加jsp依赖)
# jsp依赖
<dependency>
          <groupId>org.apache.tomcat.embed</groupId>
          <artifactId>tomcat-embed-jasper</artifactId>
      </dependency>
<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>jstl</artifactId>
</dependency>
添加配置(如果是thymeleaf要禁用缓存)
# 是否支持批量上传   (默认值 true)
spring.servlet.multipart.enabled=true
# 上传文件的临时目录 (一般情况下不用特意修改)
spring.servlet.multipart.location=
# 上传文件最大为 1M (默认值 1M 根据自身业务自行控制即可)
spring.servlet.multipart.max-file-size=1048576
# 上传请求最大为 10M(默认值10M 根据自身业务自行控制即可)
spring.servlet.multipart.max-request-size=10485760
# 文件大小阈值,当大于这个阈值时将写入到磁盘,否则存在内存中,(默认值0 一般情况下不用特意修改)
spring.servlet.multipart.file-size-threshold=0
# 判断是否要延迟解析文件(相当于懒加载,一般情况下不用特意修改)
spring.servlet.multipart.resolve-lazily=false
  • 如默认只允许1M以下的文件,当超出该范围则会抛出下述错误
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (20738021) exceeds the configured maximum (10485760)
前端(jsp或者thymeleaf都可,无技术区别,语法不同)
  • 此处用了ajax上传文件,页面不会刷新
  • 此处用ajax上传文件时,还用到了第三方文件jquery.form.js
  • ajax中的$(this).ajaxSubmit()就是由第三方文件提供,其他与mvc中没有区别,此处不使用也可以
<%@page isELIgnored="false" language="java" contentType="text/html; utf-8" pageEncoding="UTF-8" %>

<script src="/js/jquery-1.4.2.js"></script>
<script src="/js/jquery.form.js"></script>
<script>
    $(function() {
        $("#formLogin").bind("submit", function() {
            $(this).ajaxSubmit({
                //dataType: "json",           //html(默认), xml, script, json...接受服务端返回的类型
                //提交成功后的回调函数
                success : function(data, status, xhr, $form) {
                    alert(data);
                    // $("#uploadimg").attr("src",data);
                }
            });
            return false; //阻止表单默认提交
        });


        $("#manyfiles").bind("click",function () {
            $("#filea").clone().appendTo("#formLogin");
        });

    });
</script>

<body>
<h2>单一文件上传示例</h2>
<div>
    <form method="POST" enctype="multipart/form-data"
          action="/upload1">
        <p>
            文件1:<input type="file" name="file" /> <input type="submit"
                                                         value="上传" />
        </p>

    </form>
</div>

<hr />
<h2>批量文件上传示例 <input type="button" value="增加" id="manyfiles"></h2>

<div>
    <form method="POST" enctype="multipart/form-data"
          action="/upload2"  id="formLogin">
        <p id="filea">
            文件:<input type="file" name="file" />
        </p>
        <p>
            文件:<input type="file" name="file" />
        </p>
        <p>
            <input type="submit" value="上传" />
        </p>
    </form>
</div>
</body>
controller接收文件
public class FileLoadController{
	@PostMapping("/upload1")
	public Map<String, String> upload1(@RequestParam("file") MultipartFile file)
			throws IOException {
		// 将文件写入到指定目录(具体开发中有可能是将文件写入到云存储/或者指定目录通过 Nginx 进行 gzip
		// 压缩和反向代理,此处只是为了演示故将地址写成本地电脑指定目录)
		file.transferTo(new File("d:/" + file.getOriginalFilename()));
		// 获取文件对象中的属性,有个getName方法,获取的是上传文件的组件名,不是文件名,下面的才是文件名
		Map<String, String> result = new HashMap<>(16);
		result.put("contentType", file.getContentType());
		result.put("fileName", file.getOriginalFilename());
		result.put("fileSize", file.getSize() + "");
		return result;
	}

	// 多个文件上传,唯独就是单个参数变为数组,其他一致
	@PostMapping("/upload2")
	public List<Map<String, String>> upload2(
			@RequestParam("file") MultipartFile[] files) throws IOException {
		if (files == null || files.length == 0) {
			return null;
		}
		List<Map<String, String>> results = new ArrayList<>();
		for (MultipartFile file : files) {
			file.transferTo(new File("d:/" + file.getOriginalFilename()));
			Map<String, String> map = new HashMap<>(16);
			map.put("contentType", file.getContentType());
			map.put("fileName", file.getOriginalFilename());
			map.put("fileSize", file.getSize() + "");
			results.add(map);
		}
		return results;
	}
}

整合FastDFS文件上传

在前面我们学习了springboot上传文件
通过学习我们可以实现文件上传,但是依然有问题存在
在springboot运行时,我们的项目运行在一台服务器上
项目运行的服务器,也就是我们的tomcat服务器,我们可以认为就是web服务器
而一般实际开发中,几乎没有将上传文件保存在web服务器中的
而要将web服务器上传的文件保存到其他数据服务器上,这里我们就借助FastDFS实现
此时我们可以将FastDFS看做文件服务器,将web上传的文件保存到上面去
开源的文件服务器有很多,目前中小型企业用的比较多的就是FastDFS
所以我们在开始项目之前应该先将FastDFS服务器搭建好

连接FastDFS
第一步:搭建FastDFS环境(参考对应资料),然后启动服务
  • 在服务器系统中按照配置好FastDFS(很复杂,看资料)
  • 安装好之后启动其跟踪器,通过命令:
    /usr/bin/fdfs_stackerd /etc/fdfs/tracker.conf restart
    
  • 启动跟踪器后再启动文件保存节点:
    /usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
    
  • 可以通过命令先传一个文件到站点,测试FastDFS服务是否启动成功:
    /usr/bin/fdfs_test /etc/fdfs/client.conf upload /home/aaa.png
    
  • 可以看到,FastDFS提供了客户端,供测试使用。我们此处将home文件夹下的aa.png文件上传到FastDFS服务器,会保存到站点下的Data文件夹下。如果上传成功,会提示保存的路径
第二步:pom中加入依赖
<!-- FastDFS依赖 -->
<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
    <version>1.26.5</version>
</dependency>
第三步:springboot中加入配置
  • 前面文件上传的配置要保留,此处只是对FastDFS的配置
    #连接超时时间
    fdfs.connect-timeout=60
    #读取时间
    fdfs.so-timeout=60
    #生成缩略图参数(上传后会上传缩略图)
    fdfs.thumb-image.height=150
    fdfs.thumb-image.width=150
    # FastDFS所在服务器ip地址和FastDFS端口号
    # 可以在cmd中通过telnet 192.168.139.129 22122测试是否能连接到,ip与端口间空格连接
    fdfs.tracker-list=192.168.139.129:22122
    
实现文件上传、下载、删除
Controller实现
  • 文件上传

    此处上传文件,对于页面,与之前一样,没有区别,后台接受文件数据也是一样,唯一不同的是
    接受到文件后的处理不同
    直接在Controller中注入FastDFS提供给我们的工具类FastFileStorageClient
    该工具类中给我们提供了上传、下载、删除等功能,如果服务器连接不到。可能注入工具类会失败
    对于文件的上传,如果同一文件多次上传,也会成功,FastDFS会自动给上传后的文件命名,所以允许多次上传,不会覆盖

    @RestController
    public class FastdfsController {
    	// 此处直接注入这个工具类,FastDFS给我们提供的工具类
    	@Autowired
    	private FastFileStorageClient fastFileStorageClient;
    
    	/**
     	* 文件上传
     	*/
    	@PostMapping("/uppload")
    	public StorePath test(@RequestParam MultipartFile file) throws IOException {
        	// 设置文件信息
        	Set<MetaData> metaData = new HashSet<>();
        	metaData.add(new MetaData("author", "zonghui"));
        	metaData.add(new MetaData("description", "xxx文件,哈哈"));
    
        	// 上传(文件上传可不填文件信息,填入null即可)
        	StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()), metaData);
        	return storePath;
    	}
    }
    

    fastFileStorageClient.uploadFile()方法是工具类提供的上传的方法,有三个重载的方法,此处用了有四个参数的一种方式

    参数一:一个文件自己输入流,直接通过文件获取即可
    参数二:Long类型的文件大小,直接通过文件对象获取即可
    参数三:字符串类型的参数,可以作为描述,一般传入文件名称,此处是对文件名做了一些处理,如果不想传入信息,直接给null值也可以。
    参数四:一个MetaData类型的set集合,一般是自定义一些参数,作为上传时的信息等。MetaData也是FastDFS提供的一个类型,导包是要注意,该类型就是String类型的键值对,随意设置一些信息,如上面

    该uploadFile方法上传成功后会返回一个对象,该对象包含了上传成功后文件的保存位置信息,可以保存到数据库,方便后面获取下载该文件,该对象有三个属性

    属性一:group,分组,一般默认值就是group1
    属性二:path,保存的地址,一般为字符串类型的“M00/00/00/文件名”,就是在站点下的Data文件夹下
    属性三:fullPath,保存的完整路径,一般也是字符串类型的“group1/M00/00/00/文件名”,相比较上面加了分组文件夹

  • 文件删除

    此处删除同样直接调用工具类提供给我们的deleteFile方法,有两种重载的方法
    方法一:直接提供一个完整的路径,也就是上传时返回值的第三个属性fullPath属性
    方法二:提供组名和路径名,也就是上传时返回值的第一个属性group和第二个属性path
    其实两种方式是一样的。上传时的返回值中三个属性,其中fullPath就是group加上path的值
    需要注意的是该删除的方法没有返回值。直接void。

    @RestController
    public class FastdfsController {
    
    	@Autowired
    	private FastFileStorageClient fastFileStorageClient;
    
    	@DeleteMapping("/delete")
    	public String delete(@RequestParam String fullPath) {
        	// 第一种删除:参数:完整地址
        	fastFileStorageClient.deleteFile(fullPath);
        	// 第二种删除:参数:组名加文件路径
        	// fastFileStorageClient.deleteFile(group,path);
        	return "恭喜恭喜,删除成功!";
    	}
    }
    
  • 文件下载

    下载文件同样通过FastDFS提供的工具类,有downloadFile方法,且重载了两种方式,一般我们使用第一种
    fastFileStorageClient.downloadFile()方式一要求我们提供三个参数:

    参数一:上传文件时返回的文件分组,也就是group
    参数二:上传文件时返回的路径地址,也就是path
    参数三:一个DownloadCallback<? extends Object> downloadCallback参数,这是一个泛型对象,我们给定的参数的类型不同,下载方法运行后的返回值也就不同

    常用类型一:DownloadByteArray类型,也是下面我们用的类型,返回值就是字节数组
    常用类型二:DownloadFileWrite类型,返回的是文件输出流

    此处我们下载后返回的是字节数组,直接通过Spring提供的工具类FileCopyUtils类的Copy方法进行处理
    FileCopyUtils.copy()有多个重载的方法,此处我们使用两个参数的方法

    参数一:数据源,也就是我们下载成功后返回的字节数组
    参数二:一个输出流对象,就会将文件下载到我们提供的输出流对应的位置去,此处我们直接下载到浏览器,所以在controller方法中获取到Response对象,然后通过该响应对象获取到输出流进行文件下载

    response.getOutputStream()会获取到输出流,一般会输出到屏幕,如果直接获取写入copy方法,虽然可以下载成功,但是可能不会保存,甚至我们也看不到,所以要提前对响应对象进行设置:

    response.setContentType(“application/x-tar;charset=utf-8”);设置响应的主体类型,为文件下载,且为utf8格式
    response.setHeader(“Content-disposition”, “attachment;filename=”+ java.net.URLEncoder.encode(“优化.png”, “UTF-8”));
    设置响应头,以及下载后保存的文件名以及编码格式,此处我们设定死了文件名为优化.png,实际开发中因为我们上传文件时在数据库保存了文件上传地址,文件名等,所以我们会去数据库获取文件名,然后进行动态的设置

    此处通过设置响应,我们可以直接将文件下载到浏览器,时间就是我们浏览器安装是默认的下载文件的位置,比如我的电脑设置的谷歌浏览器下载文件默认地址为F://goole//down,那么就会将文件下载到本地的这个地址中去

    @RestController
    public class FastdfsController {
    
    	@Autowired
    	private FastFileStorageClient fastFileStorageClient;
    	//
    	@GetMapping("/download")
    	public void downLoad(@RequestParam String group, @RequestParam String path, HttpServletResponse response) throws IOException {
    
    		response.setContentType("application/x-tar;charset=utf-8");
    		response.setHeader("Content-disposition", "attachment;filename="+ java.net.URLEncoder.encode("优化.png", "UTF-8"));
    
        	// 获取文件
        	byte[] bytes = fastFileStorageClient.downloadFile(group, path, new DownloadByteArray());
        	FileCopyUtils.copy(bytes,response.getOutputStream());
    	}
    }
    
Postman测试
  • Postman是一款客户端模拟软件
  • 可以在不需要写前端的情况下,模拟客户端发起请求
  • 按照好软件后打开,新建一个请求
  • 然后设置请求为get或post等,然后给定请求路径
  • 将需要的参数设定好,然后send发送请求即可
模拟上传

在这里插入图片描述

模拟删除

在这里插入图片描述

模拟下载

在这里插入图片描述

整合POI、FastDFS

POI
概念
  • Apache POI项目是创造和保持java API操纵各种文件格式
  • 基于Office Open XML标准(OOXML)和微软的OLE复合文档格式(OLE2)
  • 总之,Apache POI可以使用java读写Excel文件。
  • 此外,您可以读取和写入MS Word和PowerPoint文件。
POI API结构包名称说明
  • HSSF提供读写Microsoft Excel XLS格式档案的功能。
  • XSSF提供读写Microsoft Excel OOXML XLSX格式档案的功能。
  • HWPF提供读写Microsoft Word DOC(Word文档)格式档案的功能。
  • XWPFDocument提供读写Microsoft Word DOCX格式文档。
  • HSLF提供读写Microsoft PowerPoint(PPT)格式档案的功能。
  • HDGF提供读Microsoft Visio(画图)格式档案的功能。
springboot整合POI操作
第一步:导入依赖
<!-- poi依赖 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.14</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>3.14</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-excelant</artifactId>
    <version>3.14</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml-schemas</artifactId>
    <version>3.14</version>
</dependency>
第二步:创建Excel

在这里插入图片描述

第三步:创建Excel读取工具类

此处的工具类官方提供了读取各种格式文件的demo,直接下载进行修改就可以使用
也可以自己进行实现,主要是利用POI提供的各种类型,进行数据及文件的处理

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.util.*;

public class ReadExcel {

	// 这里主函数用于测试,后面的 静态方法就是读取的工具方法
    public static void main(String[] args) throws Exception {
        List<Map<String, String>> d = readExcel("C:/Users/Administrator/Desktop/a.xls");
        for (Map<String, String> map : d) {
            Set<Map.Entry<String, String>> es = map.entrySet();
            for (Map.Entry<String, String> entry : es) {
                System.out.print(entry.getValue() + "\t");
            }
            System.out.println();
        }

    }

    /**
     * 根据excel文件路径返回list list对应一张表的数据
     * list里面是多个map集合,一个map对应一行数据
     *
     * @param fileName 带有文件扩展名的文件名称
     * @param fis 文件的字节输入流
     * @return
     * @throws Exception
     */
    public static List<Map<String, String>> readExcel(String fileName,InputStream fis) throws Exception {
        // 创建list集合,保存所有数据
        List<Map<String, String>> sheetList = new ArrayList();
		
		// Workbook对象可以认为就是一个Excel对象,或者是Excel文件
        Workbook workbook = null;
        // 因为Excel两种格式,必须根据不同的扩展名创建不同包下的对象
        if (fileName.toLowerCase().endsWith("xlsx")) {
            workbook = new XSSFWorkbook(fis);
        } else if (fileName.toLowerCase().endsWith("xls")) {
            workbook = new HSSFWorkbook(fis);
        }

		// 一个sheet对象就是Excel里面的一张表格,下标从0开始,此处就是获取Excel里面的第一个工作簿
        Sheet sheet = workbook.getSheetAt(0);

		// 获取sheet表格的所有行
        Iterator<Row> rowIterator = sheet.iterator();
        // 跳过第一行,一般我们认为第一行是表头不进行读取,如果要读取也可以
        rowIterator.next();
        
        // 跳过表头行,开始读取数据行,迭代的方式进行遍历所有行
        while (rowIterator.hasNext()) {
        
        	//	获取到一行
            Row row = rowIterator.next();

			// 获取当前行的所有列
            Iterator<Cell> cellIterator = row.cellIterator();

			// 创建一个map保存一行数据
            Map<String, String> maprow = new HashMap<String, String>();
            // 以迭代的方式遍历当前行的所有列
            while (cellIterator.hasNext()) {
            	// 获取当前列
                Cell cell = cellIterator.next();
                //获取列的类型  0numeric  1tex ,类型是枚举的,此处我们简单直接用枚举的值进行比较
                int a = cell.getCellType();
                if(a==0){ // 类型为0则表示是double类型的
                	// 获取当前累的 
                    double d = cell.getNumericCellValue();
                    // 将获取的值保存到集合。键就是当前列的索引
                    maprow.put(cell.getColumnIndex() + "", d + "");
                }else if(a==1){ // 类型值为1则表示为字符串类型的
                    String v = cell.getStringCellValue();
                    maprow.put(cell.getColumnIndex() + "", v);
                }
            }
            // 将当前行数据保存到list中
            sheetList.add(maprow);
        }
        // 遍历完所有行,返回数据集合
        return sheetList;
    }
}
第四步:通过controller简单测试

此处我们通过controller测试,但是所有的数据都是写死的

@RestController
public class PoiController {

	@RequestMapping("/test01")
	public List<Map<String,String>> test01() throws Exception {
		// 给定一个文件的完整路径
		String filePath = "e://test.xls";
		// 此处静态工具方法直接调用
		List<Map<String,String>> list = ReadExcel.readExcel(filePath,new FileInputStream(filePath));
		return list;
	}
}

全局异常处理

实际项目开发中,程序往往会发生各式各样的异常情况,特别是身为服务端开发人员的我们,总是不停的编写接口提供给前端调用,分工协作的情况下,避免不了异常的发生
如果直接将错误的信息直接暴露给用户,这样的体验可想而知,且对黑客而言,详细异常信息往往会提供非常大的帮助
在这里插入图片描述

方式一:采用try-catch的方式

手动捕获异常信息,然后返回对应的结果集
相信很多人都看到过类似的代码(如:封装成Result对象);
该方法虽然间接性的解决错误暴露的问题,同样的弊端也很明显,增加了大量的代码量,当异常过多的情况下对应的catch层愈发的多了起来,很难管理这些业务异常和错误码之间的匹配
所以最好的方法就是通过简单配置全局掌控

@RequestMapping("/myexception1")
@ResponseBody
public String myexception1(int jsp) {
	try{
		return "json数据"
	}catch(Exception e){
		return "错误";
	}
}

方式二:Spring Boot提供的解决方案

所谓全局异常,是指所有访问的controller发生的异常都可以捕获到
实际是利用SpringAOP拦截器实现的

第一步:创建全局异常处理类

创建一个GlobalExceptionHandler类(一定要spring扫描到,也就是启动类一下的包里面即可),随意定义
并添加上@RestControllerAdvice注解就可以定义出异常通知类了
也可以使用@ControllerAdvice进行注解
@RestControllerAdvice与@ControllerAdvice的区别就想@RestController和@Controller的区别
然后在定义的方法中添加上@ExceptionHandler即可实现异常的捕捉

@RestControllerAdvice
public class GlobalExceptionHandler {
	
}

这个类实际上面就是会捕获controller的异常,只要controller有异常,就执行下面的方法返回

第二步:添加具体捕获处理异常的方法

在全局异常处理类中添加具体处理方法
该类中的方法都是自定义,随意定义多个方法都可以
实际流程是当controller中有异常发生时,全局异常处理类会进行捕获
当全局异常处理类将controller执行时发生的异常捕获时,调用其中的方法进行具体的处理

@RestControllerAdvice
public class GlobalExceptionHandler {
	/**
	 * 捕获 Exception 捕获Exception异常
	 */
	@ExceptionHandler(Exception.class)
	public String runtimeExceptionHandler(HttpServletRequest request,
			final Exception e, HttpServletResponse response) {
		System.out.println("1234");
		return "出错误了....";
	}
	    /**
     * 请求方式不支持
     */
    @ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
    public AjaxResult handleException(HttpRequestMethodNotSupportedException e)
    {
        log.error(e.getMessage(), e);
        return AjaxResult.error("不支持' " + e.getMethod() + "'请求");
    }

    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public AjaxResult notFount(RuntimeException e)
    {
        log.error("运行时异常:", e);
        return AjaxResult.error("运行时异常:" + e.getMessage());
    }
}

该类中的方法任意定义,只是每个方法上面都要加上@ExceptionHandler(Exception.class)
@ExceptionHandler()该注解表示被注解的方法用于处理异常,处理的异常在注解中指定
如果发生了异常中指定的异常及其子类异常,该方法就会进行处理
所有自定义的方法都可以接收三个参数,其中Exception就是具体捕获到的异常,比如NullPointException异常
然后返回信息时可以返回异常信息,比如自定义异常的一些特殊属性及自定义异常信息等

第三步:控制层测试

只要这里访问控制层出错Exception,就会执行到上面的GlobalExceptionHandler类的runtimeExceptionHandler方法(因为runtimeExceptionHandler捕获的是Exception异常)
下面controller中两个方法,执行时如果出现异常,方法 一会被全局异常处理
方法二不会被全局处理,因为里面的异常被try捕获处理了,所以理论上该方法访问时并没有异常出现
但是如果是接收的参数异常。此时try就没有办法处理了。依然会走全局异常处理

@RestController
public class Hello3 {	
	@RequestMapping("/myexception2")
	public List<String> myexception2(int jsp) {
		List<String> list = new ArrayList<String>();
		System.out.println(10 / jsp);
		list.add("你的");
		list.add("好的");
		return list;
	}
	@RequestMapping("/myexception1")
	@ResponseBody
	public String myexception1(int jsp) {
		try{
			return "json数据"
		}catch(Exception e){
			return "错误";
		}
	}
}
自定义异常全局捕获(全局异常处理优化)
第一步:增加自定义异常
  • 自定义异常一
    //自定义异常
    public class CustomException extends RuntimeException {
    	private int code;
    	//get、set
    	public CustomException() {
    		super();
    	}
    	public CustomException(int code, String message) {
    		super(message);
    		this.setCode(code);
    	}
    }
    
  • 自定义异常二
    /**
    * 文件信息异常类
    * 
    * @author yt
    */
    public class FileException extends BaseException
    {
    	private static final long serialVersionUID = 1L;
    
    	public FileException(String code, Object[] args)
    	{
        	super("file", code, args, null);
    	}
    
    }
    
  • 自定义异常三
    /**
    * 用户密码不正确或不符合规范异常类
    * 
    * @author yt
    */
    public class UserPasswordNotMatchException extends UserException
    {
    	private static final long serialVersionUID = 1L;
    
    	public UserPasswordNotMatchException()
    	{
        	super("user.password.not.match", null);
    	}
    }
    
第二步:修改自定义异常处理类【能够捕获自定义的异常】(关键)
@RestControllerAdvice
public class GlobalExceptionHandler {
	/**
	 * 捕获 Exception 捕获Exception异常
	 */
	@ExceptionHandler(Exception.class)
	public Map<String, Object> runtimeExceptionHandler(
			HttpServletRequest request, final Exception e,
			HttpServletResponse response) {
		Map<String, Object> maperr = new HashMap<String, Object>();//以json格式返回
		//如果是返回错误页面,就可以返回ModelAndView mv = new ModelAndView();
		if (e instanceof CustomException) {// 如果捕获的是自定义的CustomException异常
			CustomException customException = (CustomException) e;
			maperr.put("code", customException.getCode());
		} else {
			maperr.put("code", 400);
		}
		maperr.put("message", e.getMessage());
		return maperr;
	}

	/**
     * 自定义验证异常
     */
    @ExceptionHandler(BindException.class)
    public AjaxResult validatedBindException(BindException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return AjaxResult.error(message);
    }

	/**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Object businessException(HttpServletRequest request, BusinessException e)
    {
        log.error(e.getMessage(), e);
        if (ServletUtils.isAjaxRequest(request))
        {
            return AjaxResult.error(e.getMessage());
        }
        else
        {
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.addObject("errorMessage", e.getMessage());
            modelAndView.setViewName("error/business");
            return modelAndView;
        }
    }
}
第三步:控制层测试
@RestController
public class Hello3 {
	@RequestMapping("/myexception2")
	@CrossOrigin(origins = "*")
	public List<String> myexception2(int jsp) {
		List<String> list = new ArrayList<String>();
		if (jsp == 0) {
			throw new CustomException(401, "***错误了");//这里抛出自定义异常
		}
		System.out.println(10 / jsp);
		list.add("你的");
		list.add("好的");
		return list;
	}
总结:

在这里插入图片描述
实际上到这里为止,我们后面所有的controller返回json的时候,都可以返回一个map集合了
map集合必须有code和message两个属性,可以当做客户端调用的依据

  • 下面是若依系统自定义信息操作类,就是继承了Map集合
**
 * 操作消息提醒
 *
 * @author yt
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;
    /** 状态码 */
    public static final String CODE_TAG = "code";
    /** 返回内容 */
    public static final String MSG_TAG = "msg";
    /** 数据对象 */
    public static final String DATA_TAG = "data";
    /**
     * 状态类型
     */
    public enum Type
    {
        /** 成功 */
        SUCCESS(0),
        LZSUCCESS(1),
        LZERROR(-1),
        NOLOGIN(-2),
        NOFA(-10),
        /** 警告 */
        WARN(501),
        WARNMSG(502),
        /** 错误 */
        ERROR(500);
        private final int value;
        Type(int value)
        {
            this.value = value;
        }
        public int value()
        {
            return this.value;
        }
    }
    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }
    /**
     * 初始化一个新创建的 AjaxResult 对象
     * @param type 状态类型
     * @param msg 返回内容
     */
    public AjaxResult(Type type, String msg)
    {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
    }
    /**
     * 初始化一个新创建的 AjaxResult 对象
     * @param type 状态类型
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(Type type, String msg, Object data)
    {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
        if (StringUtils.isNotNull(data))
        {
            super.put(DATA_TAG, data);
        }
    }
    /**
     * 方便链式调用
     * @param key 键
     * @param value 值
     * @return 数据对象
     */
    @Override
    public AjaxResult put(String key, Object value)
    {
        super.put(key, value);
        return this;
    }
    /**
     * 返回成功消息
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }
    /**
     * 返回成功数据
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }
    /**
     * 返回成功消息
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }
    /**
     * 返回成功消息
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(Type.SUCCESS, msg, data);
    }
    /**
     * 返回成功消息
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult lzsuccess(String msg, Object data)
    {
        return new AjaxResult(Type.LZSUCCESS, msg, data);
    }
    /**
     * 返回成功消息
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult lzsuccess(Object data)
    {
        return new AjaxResult(Type.LZSUCCESS, "OK", data);
    }
    /**
     * 返回警告消息
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult warn(String msg)
    {
        return AjaxResult.warn(msg, null);
    }
    /**
     * 返回警告消息
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult warnmsg(String msg)
    {
        return AjaxResult.warnmsg(msg, null);
    }
    /**
     * 返回警告消息
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult warnmsg(String msg, Object data)
    {
        return new AjaxResult(Type.WARNMSG, msg, data);
    }
    /**
     * 返回警告消息
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult warn(String msg, Object data)
    {
        return new AjaxResult(Type.WARN, msg, data);
    }
    /**
     * 返回错误消息
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }
    /**
     * 返回错误消息
     * @return
     */
    public static AjaxResult lzerror(String msg)
    {
        return new AjaxResult(Type.LZERROR,msg,"");
    }
    /**
     * 返回错误消息
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }
    /**
     * 返回错误消息
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(Type.ERROR, msg, data);
    }
}

springboot RestFul

概念

什么是RestFul
  • Restful风格API接口开发:Restful风格的API是一种软件架构风格,只是提供了一组设计原则和约束条件。
  • 它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

在Restful风格中,用户请求的url使用同一个url而用请求方式:get,post,delete,put…等方式对请求的处理方法进行区分,这样可以在前后台分离式的开发中使得前端开发人员不会对请求的资源地址产生混淆和大量的检查方法名的麻烦,形成一个统一的接口。

RestFul规定

在RestFul风格中,有如下规定:

  • GET(SELECT):从服务器查询,可以在服务器通过请求的参数区分查询的方式。
  • POST(CREATE):在服务器新建一个资源,调用insert操作。
  • PUT(UPDATE):在服务器更新资源,调用update操作。 更新所有字段
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性,也就是更新部分字段)。(目前jdk7未实现,tomcat7也不行)。
  • DELETE(DELETE):从服务器删除资源,调用delete语句。

访问同一个资源,提交请求的方式不一样,执行的操作也不一样
如当前url是 http://localhost:8080/User
那么用户只要请求这样同一个URL就可以实现不同的增删改查操作,例如

  • http://localhost:8080/User?_method=get&id=1001 这样就可以通过get请求获取到数据库 user 表里面 id=1001 的用户信息
  • http://localhost:8080/User?_method=post&id=1001&name=zhangsan 这样可以向数据库 user 表里面插入一条记录
  • http://localhost:8080/User?_method=put&id=1001&name=lisi 这样可以将 user表里面 id=1001 的用户名改为lisi
  • http://localhost:8080/User?_method=delete&id=1001 这样用于将数据库 user 表里面的id=1001 的信息删除
  • 这样定义的规范我们就可以称之为restful风格的API接口,我们可以通过同一个url来实现各种操作。
    在这里插入图片描述

springboot实现

springboot提供的注解
@GetMapping(value="/xxx")
处理 Get 请求,等价于
@RequestMapping(value = "/xxx",method = RequestMethod.GET)

@PostMapping(value="/xxx")
处理 Post 请求,等价于
@RequestMapping(value = "/xxx",method = RequestMethod.POST)

@PutMapping(value="/xxx")
⽤于更新资源,等价于
@RequestMapping(value = "/xxx",method = RequestMethod.PUT)

@DeleteMapping(value="/xxx")
处理删除请求,等价于
@RequestMapping(value = "/xxx",method = RequestMethod.DELETE)

@PatchMapping(value="/xxx")
⽤于更新部分资源,等价于
@RequestMapping(value = "/xxx",method = RequestMethod.PATCH)
代码实现(采用postman测试)
// 采用占位符方式传入id,查询单个
@GetMapping("/user/{id}")
public String userById(@PathVariable("id") int id){
	// 访问地址为:http://localhost:8080/user/3
    return "GetMapping"+id;
}
// 不需要参数,查询所有
@GetMapping("/user")
public String userAll(){
	// 访问地址为:http://localhost:8080/user
    return "GetMapping";
}

@PostMapping("/user")
public String user(int a){
	// 访问地址为:http://localhost:8080/user?a=5
    return "PostMapping";
}

在这里插入图片描述

CORS解决跨域

概念

CORS

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。

同源策略

是由NetScape提出的一个著名的安全策略。
源(origin)就是协议、域名和端口号,所谓的同源,指的是协议,域名,端口相同。
浏览器处于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读写对方的资源。
http://localhost:8080/user
同源政策规定,AJAX请求只能发给同源的网址,否则就报错,有三种方法规避这个限制:
JSONP
WebSocket
CORS
在这里插入图片描述

springboot实现CORS

第一步:创建项目
创建项目A
  • 创建一个controller提供客户端访问
    @RestController
    public class CorsController{
    
    	@RequestMapping("/test1")
    	public String test1(int a){
    		return "aa";
    	}
    }
    
  • 创建一个jsp页面,使用ajax请求对应后台的controller
    <%@page language="java" pageEncode="UTF-8" contentType="text/html;utf-8" %>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>html</html>
    	<!--引入jQuery文件-->
    	<script src="/js/jquery-1.4.2.js"></script>
    
    	<!-- ajax发送请求-->
    	<script>
    		function subcors() {
    			$.get("http://localhost:8080/test1",function (data){
    				alert(data);
    			});
    		}
    	</script>
    </head>
    <body>
    	<input type="button" value="访问同源资源" onclick="subcors()">
    </body>
    </html>
    
  • 启动项目,访问jsp页面,点击按钮,正常访问,可以看到弹出框打印的内容
创建项目B
  • 创建jsp访问项目A的controller ,不需要其他东西,无论项目B端口,保持访问的路径与项目A一致
    <%@page language="java" pageEncode="UTF-8" contentType="text/html;utf-8" %>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>html</html>
    	<!--引入jQuery文件-->
    	<script src="/js/jquery-1.4.2.js"></script>
    
    	<!-- ajax发送请求-->
    	<script>
    		function subcors() {
    			$.get("http://localhost:8080/test1",function (data){
    				alert(data);
    			});
    		}
    	</script>
    </head>
    <body>
    	<input type="button" value="访问同源资源" onclick="subcors()">
    </body>
    </html>
    
  • 设置端口,项目A默认为8080,项目B设置为8081即可

    可以在springboot配置文件中设置项目端口
    此处在整个项目配置中设置,效果一样,两种方式任选一种即可
    在这里插入图片描述

  • 启动项目,访问jsp页面,点击按钮进行ajax请求
  • 点击后会发现浏览器控制台报错信息,报错如下:
    在这里插入图片描述
  • 访问失败就是因为不同源的原因,该项目端口为8081,访问的地址端口却是8080,自然访问不到
springboot解决跨域

@CrossOrigin注解解决跨域问题
只需要在原来的controller上面加上该注解即可
注解的值里面的星号*表示允许任何源请求访问,也可以指定特定的请求访问,该值是数组形式,可以指定多个值

@RequestMapping("/test1")
@CrossOrigin(origins = "*") //允许不同源ajax请求
public String test1(int a){
    return "aa";
}
  • 此时刷新项目B,请求jsp页面,点击按钮访问controller,会发现与项目A中访问一样,没有任何问题

springboot启动任务

传统方式

在 Servlet/Jsp 项目中,如果涉及到系统任务,例如在项目启动阶段要做一些数据初始化操作,这些操作有一个共同的特点,只在项目启动时进行,以后都不再执行,这里,容易想到web基础中的三大组件( Servlet、Filter、Listener )之一 Listener ,这种情况下,一般定义一个 ServletContextListener,然后就可以监听到项目启动和销毁,进而做出相应的数据初始化和销毁操作

public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        //在这里做数据初始化操作
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        //在这里做数据备份操作
    }
}

springboot方式

这是基础 web 项目的解决方案,这种方式在springboot中也是可以使用的
如果使用了 Spring Boot,那么我们可以使用更为简便的方式。

单个启动任务:

自定义 MyCommandLineRunner1 并且实现 CommandLineRunner 接口
该类需要定义在启动类下面的包里面,让springboot加载到
该类上面需要加上@Component注解,加载到spring容器中
此时这个类就是一个启动任务类,当启动web容器的时候,就会自动执行这个类的run方法

@Component
public class MyCommandLineRunner1 implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("系统启动任务....");
    }
}
多个启动任务设置优先级:

也就是当项目中有多个启动任务类存在时,多个启动任务类都会执行
但是如果我们希望多个启动任务类的执行按照我们设定的顺序执行
那么就需要我们提前设置多个启动任务类的执行优先级
只需要在启动任务类上面加上@Order注解,该注解表示排序,其中有int类型的value属性
@Order注解中的数值越小,优先级越高,默认值为Integer.MAX_VALUE,优先级最低

/**
 * 第一个启动任务类
 */
@Component
@Order(value = 10) //value值越小,先执行
public class A implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("启动任务类B");
    }
}


/**
 * 第二个启动任务类
 */
@Component
@Order(value = 5) //value值越小,先执行
public class B implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("启动任务类B");
    }
}
启动任务设置作用域:

假设我们想在启动类中操作servletAPI
比如我们想在作用域里面设置一些变量的值
比如我们想在启动任务类中run方法执行时,加载一下对象,而这些对象在后续任何请求中都可以用到
那么此时我们希望将这些对象放置在某一个作用域当中去,比如全局作用域ServletContext作用域
有了ServletContext作用域,其中的变量,在整个web容器中都可以使用
所以此处我们要做的就是怎么在启动任务类中使用这些作用域的问题
只需要将需要使用的作用域对象注入就可以正常使用了,这样后续这些作用域当中初始化的东西都可以使用

@Component
@Order(value = 22)
public class A implements CommandLineRunner {

	// 自动注入ServletContext作用域对象
    @Autowired
    ServletContext application;
    
	@Override
    public void run(String... args) throws Exception {
		// 比如执行了相关初始化操作,把数据存储到ServletContext作用域中
		application.setAttribute("abc","abc的值");
		// 该作用域是整个web容器,所以此时我们项目启动后在任何位置都可以进行访问
        System.out.println("启动任务类B");
    }
}

新建jsp进行访问全局作用域中的值,会发现能够正常取出使用

<%@page language="java" pageEncode="UTF-8" contentType="text/html;utf-8" %>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>html</html>
</head>
<body>
	<!--访问启动任务类中在全局作用域中设置的值-->
	${applicationScope.abc}
</body>
</html>

springboot整合swagger

基本概念

Swagger 是⼀系列 RESTful API 的⼯具,通过 Swagger 可以获得项⽬的⼀种交互式⽂档,客户端 SDK 的⾃
动⽣成等功能。
通过扫描代码去生成描述文件,连描述文件都不需要再去维护了。所有的信息,都在代码里面了。代码即接口文档,接口文档即代码
通俗的讲,就是生成项目相关的接口使用手册

环境搭建

第一步:引入依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.8.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.8.0</version>
</dependency>
第二步:SwaggerConfig配置类
注意
  • 在启动类下面任意包下创建代码,让spring扫描到,一般新建一个configuration包
  • 创建配置类类名随意,这里用SwaggerConfig命名
  • 类上面添加注解Configuration,指启动时加载,该注解为spring框架注解
  • 类上面添加注解EnableSwagger2,表示此项目启用SwaggerAPI文件,该注解为springbfox包注解
  • 类中有两个方法,都是固定写法,Swagger官方文档上面有的,直接复制进行修改就可以了
  • 其中的api方法上面添加Bean注解,该注解为spring框架注解,除此之外类中其他引入都是springfox包下的类
  • 在api方法中,需要修改basePackage参数为我们需要扫描生成文档的包,一般是项目中的controller包
  • apiInfo方法主要就是设置生成的文档上面的信息,都是根据项目进行修改的
代码
// @Configuration,启动时加载此类
// @EnableSwagger2,表示此项⽬启⽤ Swagger API ⽂档
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        //注意的是
        // .apis(RequestHandlerSelectors.basePackage("com.neo.xxx")) 指定需要扫描的包路径,只有此路径下的
        // Controller 类才会⾃动⽣成 Swagger API ⽂档
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // ⾃⾏修改为⾃⼰的包路径
                .apis(RequestHandlerSelectors.basePackage("com.ccc.demoboot.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 这块配置相对重要⼀些,主要配置⻚⾯展示的基本信息包括,标题、描述、版本、服务条款、联系⽅式等
     *
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("客户管理")
                .description("客户管理中⼼ API 1.0 操作文档")
                //服务条款⽹址,可以将该API部署到服务器,此处写访问的url地址,这样外网也可以访问该API文档
                .termsOfServiceUrl("http://www.aa.com/")
                .version("1.0")
                // 可以写公司名称,公司官网,公司邮箱
                .contact(new Contact("你的微笑", "http://www.bb.com/", "ccc1029@126.com"))
                .build();
    }
}
第三步:启动测试
  • 在浏览器中输⼊⽹址http://localhost:8080/swagger-ui.html
  • 即可看到上⾯的配置信息,效果如下
    在这里插入图片描述
  • 如果访问地址后,发现⻚⾯存在这样⼀句话:No operations defined in spec!,意思是没有找到相关的 API 内
    容,这是因为还没有添加对应的 Controller 信息
  • Swagger 通过注解表明该接⼝会⽣成⽂档,包括接⼝名、请求⽅法、参数、返回信息等
  • 接下来就是在对应的controller上面添加相应注解即可

常用注解

Swagger 通过注解表明该接⼝会⽣成⽂档,包括接⼝名、请求⽅法、参数、返回信息等,常⽤注解内容如下

常用注解
作用范围API使用位置
协议集描述@API用于Controller类上
协议描述@APIOperation用于Controller方法上
非对象参数集@ApiImplicitParams用于Controller方法上
非对象参数描述@ApiImplicitParam用在@APIImplicitParams的方法里面
响应集@ApiResponses用于Controller的方法上
响应信息参数@ApiResponse用在@ApiResponses 的方法里面
描述返回对象的意义@ApiModel用在返回对象上面
对象属性@ApiModelProperty用在出入参数对象的字段上
详细介绍
@Api 作用类上面

Api 作⽤在 Controller 类上,做为 Swagger ⽂档资源,该注解将⼀个 Controller(Class)标注为一个Swagger 资源(API)。
在默认情况下,Swagger-Core 只会扫描解析具有 @Api 注解的类,⽽会⾃动忽略其他类别资源(JAX-RS endpoints、Servlets 等)的注解

@Api(value = "消息", description = "消息操作 API", position = 100, protocols = "http")
@Controller
public class SwaggerController {

在这里插入图片描述

@ApiOperation作用方法上面

把不同的controller里面的方法按照tags组合在一组,和@Api在同一级

@GetMapping("/a")
@ApiOperation(value="获取用户信息",tags={"获取用户信息copy"},notes="注意问题点")
public void a(){

}

在这里插入图片描述

@ApiImplicitParams() 用于方法

可以包含多个 @ApiImplicitParam ,对方法的参数进行描述
如果只有一个参数,可以直接使用@ApiImplicitParam
如果是多个参数,要使用如果只有一个参数,可以直接使用@ApiImplicitParams,里面包含多个@ApiImplicitParam
name–参数名称
value–参数说明
dataType–数据类型
paramType–参数类型
example–举例说明

// 如果只有一个参数,可以直接使用@ApiImplicitParam
@ApiImplicitParam(name="id",value="用户id值",example="/test?id=4")
@GetMapping("/test")
public String getUserById(int id) {
}

//如果是多个参数,要使用如果只有一个参数,可以直接使用@ApiImplicitParams,里面包含多个@ApiImplicitParam
@ApiImplicitParams({@ApiImplicitParam(name="id",value="用户id"),@ApiImplicitParam(name="name",value="用户姓名")})
@PutMapping("/test")
public String updateUser(int id,String name) {
}

在这里插入图片描述

springboot实现websocket

基本概念

什么是WebSocket

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

Http协议

通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

实现步骤

SSM实现
增加依赖
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>5.3.8</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>5.3.8</version>
</dependency>
增加WebSocket配置类
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter getServerEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
增加获取HTTPSession工具类
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        // 获取到httpsession后存储到配置对象中,这里 这个sec与配置中EndPointConfig一直,endPointConfig继承了sec类
        // 此处是键值对map,键唯一即可,此处使用该对象的字节码名字
        sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}
增加聊天消息类
public class ResultMessage {
    private boolean isSystem;
    private String fromName;
    private Object message;//如果是系统消息是数组

    public boolean getIsSystem() {
        return isSystem;
    }

    public void setIsSystem(boolean isSystem) {
        this.isSystem = isSystem;
    }

    public String getFromName() {
        return fromName;
    }

    public void setFromName(String fromName) {
        this.fromName = fromName;
    }

    public Object getMessage() {
        return message;
    }

    public void setMessage(Object message) {
        this.message = message;
    }
}
增加聊天消息处理类
public class WebSocketUtil {
    /**
     * 系统消息格式:{"isSystem":true,"fromName":null,"message","你好"}
     * 推送给某一个的消息格式:{"isSystem":true,"fromName":"张三","message",["李四","王五"]}
     */
    public static String getMessage(boolean isSystemMessage,String fromName, Object message) {
        try {
            ResultMessage result = new ResultMessage();
            result.setIsSystem(isSystemMessage);
            result.setMessage(message);
            if(fromName != null) {
                result.setFromName(fromName);
            }
            ObjectMapper mapper = new ObjectMapper();

            return mapper.writeValueAsString(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
}
增加Bean对象获取类(WebSocket类中无法注入对象,只能通过该类获取)
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}
增加PointChat类,处理连接以及消息
/**
 * 用来管理连接
 * 该类的每个对象代表客户端的一个连接,如两人聊天,就有两个该对象
 *
 * @author: 邪灵
 * @date: 2021/9/6 20:10
 * @version: 1.0
 */
@ServerEndpoint(value = "/chat/{itemId}",configurator = GetHttpSessionConfig.class)
public class ChatPoint{
    /**用来存储每一个客户端对象对应的chatEndPoint对象 */
    private static Map<Integer,Map<String,ChatPoint>> onLineUsers = new ConcurrentHashMap<>();
    /**session对象,通过该对象可以发送消息给指定用户*/
    private Session session;
    /**声明一个HTTPSession对象,我们之前登陆时里面存储了用户名*/
    private HttpSession httpSession;
    private Integer auctionItemId;

    private static AuctionRecordService auctionRecordService;

    private static Map<Integer,MyThread> threadMap=  new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(@PathParam("itemId")Integer itemId, Session session, EndpointConfig config) {
        // 将session对象赋值给对象的属性
        this.session = session;
        auctionItemId = itemId;
        if (auctionRecordService==null) {
            auctionRecordService = (AuctionRecordService) SpringUtil.getBean("auctionRecordServiceImpl");
        }
        // 我们在getHttpSessionConfig中将HTTPSession存到了sec中,
        // 此处config继承了sec,可以直接取出,用来获取登录时session中的对象
        // 拿到对象后直接赋值给属性
        this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        // 将当前对象存储到容器中,键值对,键可以使用登录的用户名或者id值,唯一即可
        Users users = (Users) httpSession.getAttribute("user");
        // 如果该键已经存在,则表示该id代表的物品已经有聊天室
        // 只需要将当前用户存入对应的map中
        if (onLineUsers.containsKey(itemId)) {
            Map<String, ChatPoint> pointMap = onLineUsers.get(itemId);
            pointMap.put(users.getName(), this);
            onLineUsers.put(itemId,pointMap);
        } else  {
            // 不存在,在新建map,存入对应的键中
            Map<String,ChatPoint> pointMap = new ConcurrentHashMap<>();
            pointMap.put(users.getName(), this);
            onLineUsers.put(itemId,pointMap);
        }
        // 拿到将要推送的数据message
        String message = WebSocketUtil.getMessage(true,null,onLineUsers.get(itemId).keySet());
        // 将数据推送给所有进入聊天室的人
        onLineUsers.get(itemId).forEach((k,v)->{
            try {
                v.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
    @OnMessage
    public void onMessage(String message,Session session) {
        // 记录喊价时间
        Date date = new Date();
        Users users = (Users) httpSession.getAttribute("user");
        // 将消息记录保存到数据库中
        AuctionRecord auctionRecord = new AuctionRecord();
        auctionRecord.setAuctions(new Auctions(auctionItemId));
        auctionRecord.setUsers(users);
        auctionRecord.setBidTime(new Date());
        Pattern pattern = Pattern.compile("[^0-9]");
        auctionRecord.setBidPrice(Double.parseDouble(pattern.matcher(message).replaceAll("").trim()));
        auctionRecordService.saveRecord(auctionRecord);
        // 创建线程
        auctionProduct(date,WebSocketUtil.getMessage(true,users.getName(),Double.parseDouble(pattern.matcher(message).replaceAll("").trim())),users);
        // 将消息转发到同一拍卖场所有用户
        onLineUsers.get(auctionItemId).forEach((k,v)->{
            try {
                if (!users.getName().equals(k)){
                    v.session.getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
    @OnClose
    public void onClose() {
        Users users = (Users) httpSession.getAttribute("user");
        // 用户离线,从容器中删除即可
        onLineUsers.get(auctionItemId).remove(users.getName());
        System.out.println("此时剩余用户数量:"+onLineUsers.get(auctionItemId).size());
        if (onLineUsers.get(auctionItemId).size()==0) {
            onLineUsers.remove(auctionItemId);
        } else {
            // 如果还有用户,系统推送消息到客户端
            onLineUsers.forEach((k,v)-> {
                v.forEach((n,c)->{
                    try {
                        c.session.getBasicRemote().sendText(WebSocketUtil.getMessage(true, null, onLineUsers.get(auctionItemId).keySet()));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            });
        }
        // 删除用户后需要判断是否有其他用户,如果已经没有,那么按照时间让系统发送数据或者流拍
    }
    private void auctionProduct(Date date,String message,Users users) {
        if (threadMap.get(auctionItemId)!=null) {
            threadMap.get(auctionItemId).setFlag(false);
        }
        MyThread thread = (MyThread) SpringUtil.getBean("myThread");
        thread.setDate(date);
        thread.setFlag(true);
        thread.setUsers(users);
        Collection<ChatPoint> values = onLineUsers.get(auctionItemId).values();
        List<Session> list = new ArrayList<>();
        for (ChatPoint value : values) {
            list.add(value.session);
        }
        thread.setList(list);
        thread.setMessage(message);
        thread.setAuctions(new Auctions(auctionItemId));
        threadMap.put(auctionItemId,thread);
        thread.start();
    }
}
增加PointChat中用到的线程类
/**
 * 线程类,用于判断落锤
 *
 * @author: 邪灵
 * @date: 2021/9/16 15:22
 * @version: 1.0
 */
@Component
@Scope("prototype")
public class MyThread extends Thread{

    private boolean flag;

    private Date date;

    private List<Session> list;

    private String message;

    private Auctions auctions;

    private Users users;

    @Autowired
    private AuctionItemService auctionItemService;

    @Autowired
    private AuctionsService auctionsService;

    @Autowired
    private OrderService orderService;

    public MyThread() {
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public List<Session> getList() {
        return list;
    }

    public void setList(List<Session> list) {
        this.list = list;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Auctions getAuctions() {
        return auctions;
    }

    public void setAuctions(Auctions auctions) {
        this.auctions = auctions;
    }

    public Users getUsers() {
        return users;
    }

    public void setUsers(Users users) {
        this.users = users;
    }

    @Override
    public void run() {
        ResultMessage resultMessage = null;
        boolean flg = true;
        auctions = auctionsService.getAuctionsById(auctions.getId());
        try {
            resultMessage = new ObjectMapper().readValue(message, ResultMessage.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        while (flag) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (flg && DateString.isAfter01(date)) {
                flg = false;
                message = WebSocketUtil.getMessage(resultMessage.getIsSystem(),resultMessage.getFromName(),resultMessage.getMessage());
                list.forEach(s->{
                    try {
                        s.getBasicRemote().sendText(message);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
            if (DateString.isAfter02(date) || auctions.getEndTime().before(new Date())) {
                message = WebSocketUtil.getMessage(resultMessage.getIsSystem(),"all"+resultMessage.getFromName(),resultMessage.getMessage());
                flag = false;
                list.forEach(s->{
                    try {
                        s.getBasicRemote().sendText(message);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
                AuctionItem auctionItem = auctions.getAuctionItem();
                auctionItem.setAudit("已拍出");
                auctionItemService.updateAudit(auctionItem);
                auctions.setEndPrice(Double.parseDouble(resultMessage.getMessage()+""));
                auctionsService.auctioned(auctions);
                orderService.generateOrders(auctions,Double.parseDouble(resultMessage.getMessage()+""),users);
            }
        }
    }
}
增加页面信息
  • 增加HTML页面
    <html>
    <head>
    	<base href="<%=basePath%>"/>
    	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    	<title></title>
    	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
    	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@bootcss/v3.bootcss.com@1.0.14/examples/dashboard/dashboard.css">
    	<link rel="stylesheet" href="/css/chatPage.css">
    	<style>
        	#subm {right: 70px !important;}
    	</style>
    </head>
    <body style="padding: 0;">
    <img style="width:100%;height:100%" src="/images/chat_bg.jpg">
    
    <div class="abs cover contaniner">
    	<div class="abs cover pnl">
        	<div class="top pnl-head" style="padding: 20px ; color: white;" >
            	<input type="hidden" value="${user.name}" id="hiddipt1">
            	<input type="hidden" value="${itemId}" id="hiddipt2">
            	<div id="userName"> 用户:${user.name}<%--<span style='float: right;color: green'>在线</span>--%><button type="button" id="close" class="close">&times;</button></div>
            	<div id="chatMes" style="text-align: center;color: #6fbdf3;font-family: 新宋体">
                	<!--正在和 <font face="楷体">张三</font> 聊天-->
            	</div>
        	</div>
        	<!--聊天区开始-->
        	<div class="abs cover pnl-body" id="pnlBody" >
            	<div class="abs cover pnl-left" id="initBackground" style="background-color: white; width: 100%">
                	<div class="abs cover pnl-left" id="chatArea">
                    	<div class="abs cover pnl-msgs scroll" id="show">
                        	<div class="pnl-list" id="hists"><!-- 历史消息 --></div>
                        	<div class="pnl-list" id="msgs">
    
                            	<!-- 消息这展示区域 -->
                            	<%--<div class="msg guest"><div class="msg-right"><div class="msg-host headDefault"></div><div class="msg-ball">你好</div></div></div>
                            	<div class="msg robot"><div class="msg-left" worker=""><div class="msg-host photo" style="background-image: url(/images/Member002.jpg)"></div><div class="msg-ball">你好</div></div></div>--%>
                        	</div>
                    	</div>
    
                    	<div class="abs bottom pnl-text">
                        	<div class="abs cover pnl-input">
                            	<textarea class="scroll" id="context_text" wrap="hard" placeholder="在此输入文字信息..."></textarea>
                            	<div class="abs atcom-pnl scroll hide" id="atcomPnl">
                                	<ul class="atcom" id="atcom"></ul>
                            	</div>
                        	</div>
    
                        	<div class="abs br pnl-btn" id="submit" style="background-color: rgb(32, 196, 202); color: rgb(255, 255, 255);">
                            发送
                        	</div>
                        	<div class="abs br pnl-btn" id="subm" style="background-color: rgb(32, 196, 202); color: rgb(255, 255, 255);">
                            自动
                        	</div>
                        	<%--<div class="pnl-support" id="copyright"><a href="http://www.itcast.cn"></a></div>--%>
                    	</div>
                	</div>
    
                	<!--聊天区 结束-->
    
                	<div class="abs right pnl-right">
                    	<div class="slider-container hide"></div>
                    	<div class="pnl-right-content">
                        	<div class="pnl-tabs">
                            	<div class="tab-btn active" id="hot-tab">竞拍人员</div>
                        	</div>
                        	<div class="pnl-hot">
                            	<ul class="rel-list unselect" id="userlist">
                                	<%--<li class="rel-item"><a onclick='showChat("张三")'>张三</a></li>
                                	<li class="rel-item"><a onclick='showChat("李四")'>李四</a></li>--%>
                            	</ul>
                        	</div>
                    	</div>
    
                    	<div class="pnl-right-content">
                        	<div class="pnl-tabs">
                            	<div class="tab-btn active">系统广播</div>
                        	</div>
                        	<div class="pnl-hot">
                            	<ul class="rel-list unselect" id="broadcastList">
                                	<%--<li class="rel-item" style="color: #9d9d9d;font-family: 宋体">您的好友 张三 已上线</li>
                                	<li class="rel-item" style="color: #9d9d9d;font-family: 宋体">您的好友 李四 已上线</li>--%>
                            	</ul>
                        	</div>
                    	</div>
                	</div>
            	</div>
        	</div>
    	</div>
    </div>
    </body>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
    <script src="http://pv.sohu.com/cityjson?ie=utf-8"></script>
    <script src="/js/chatPage.js"></script>
    <script src="https://eqcn.ajz.miesnfu.com/wp-content/plugins/wp-3d-pony/live2dw/lib/L2Dwidget.min.js"></script>
    <!--小帅哥: https://unpkg.com/live2d-widget-model-chitose@1.0.5/assets/chitose.model.json-->
    <!--萌娘:https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json-->
    <!--小可爱(女):https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json-->
    <!--小可爱(男):https://unpkg.com/live2d-widget-model-haruto@1.0.5/assets/haruto.model.json-->
    <!--初音:https://unpkg.com/live2d-widget-model-miku@1.0.5/assets/miku.model.json-->
    <!-- 上边的不同链接显示的是不同的小人,这个可以根据需要来选择 下边的初始化部分,可以修改宽高来修改小人的大小,或者是鼠标移动到小人上的透明度,也可以修改小人在页面出现的位置。 -->
    <script>
    	/*https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json*/
    L2Dwidget.init({ "model": { jsonPath:
                "https://unpkg.com/live2d-widget-model-miku@1.0.5/assets/miku.model.json",
            "scale": 1 }, "display": { "position": "right", "width": 330, "height": 450,
            "hOffset": 0, "vOffset": -20 }, "mobile": { "show": true, "scale": 0.5 },
        "react": { "opacityDefault": 0.8, "opacityOnHover": 0.1 } });
    </script>
    </html>
    
  • 增加背景图片
    在这里插入图片描述
  • 增加CSS样式
    此处样式文件代码较多,可从gitee远程仓库项目中下载使用
    
  • 增加JavaScript处理信息
    var toName;
    var isEnd = false;
    var isPointer = false;
    function showChat(name) {
    	toName = name;
    	//清除聊天区的数据
    	$("#msgs").html("");
    	//现在聊天对话框
    	$("#chatArea").css("display","inline");
    	//显示“正在和谁聊天”
    	$("#chatMes").html("正在和 <font face=\"楷体\">"+toName+"</font> 聊天");
    }
    $(function () {
    	// 创建websocket对象
    	var itemId = $('#hiddipt2').val();
    	var userName = $('#hiddipt1').val();
    	var socket = new WebSocket('ws://localhost:8095/chat/'+itemId);
    	// 绑定事件
    	socket.onopen=function () {
        	// 连接建立后触发
        	var price = sessionStorage.getItem('price');
        	if (price!=null && price!=undefined) {
            	var str = "<div class=\"msg robot\"><div class=\"msg-left\" worker=\"\"><div class=\"msg-host photo\" style=\"background-image: url(/images/Member002.jpg)\"></div><div class=\"msg-ball\">"+res.message+"</div></div></div>";
            	$('#msgs').append(str);
        	}
        	$.get("getStartPrice",{itemId:itemId},function (auctions) {
            	var auctionsId = JSON.parse(auctions);
            	$("#chatMes").html("<font face=\"楷体\">"+auctionsId.auctionItem.name+"</font>起拍价:<font face=\"楷体\">"+auctionsId.startPrice+"</font>");
        	});
    	}
    	// 接收到服务端发送的数据后触发
    	socket.onmessage=function (event) {
        	// 带有事件参数,通过该事件对象获取服务端发送的数据
        	var res = JSON.parse(event.data);
        	if (res.isSystem) {
            	if (res.fromName.indexOf('all')==0) {
                	$('#broadcastList').html("<li class=\"rel-item\" style=\"color: #ff0000;font-family: 宋体\">恭喜"+res.fromName.substring(3)+"以"+res.message+"价格拍得</li>");
                	$('#broadcastList').append("<li class=\"rel-item\" style=\"color: #ff0000;font-family: 宋体\">本次拍卖会到此结束</li>");
                	isEnd = true;
            	} else if (res.fromName!=null) {
                	$('#broadcastList').html("<li class=\"rel-item\" style=\"color: #ff0000;font-family: 宋体\">"+res.fromName+"出价"+res.message+",感兴趣者请继续出价</li>");
            	} else {
                	// 人员列表
                	var userList = "";
                	// 系统消息
                	var broadcastListStr = "";
                	// 获取人员列表
                	var names = res.message;
                	for (var name of names) {
                    	if (userName!=name) {
                        	userList+="<li class=\"rel-item\"><a οnclick='showChat(\"张三\")'>"+name+"</a></li>";
                        	broadcastListStr+="<li class=\"rel-item\" style=\"color: #9d9d9d;font-family: 宋体\">竞拍用户 "+name+" 进入拍卖现场</li>";
                    	}
                	}
                	$('#userlist').html(userList);
                	$('#broadcastList').html(broadcastListStr);
            	}
        	} else {
            	var str = "<div class=\"msg robot\"><div class=\"msg-left\" worker=\"\"><div class=\"msg-host photo\" style=\"background-image: url(/images/Member002.jpg)\"></div><div class=\"msg-ball\">"+res.message+"</div></div></div>";
            	$('#msgs').append(str);
            	sessionStorage.setItem('price',res.message);
            	$("#submit").removeAttr('title');
            	isPointer = false;
            	$("#submit").css({"pointer-events": "auto" });
        	}
    	}
    
    	socket.onerror=function () {
        	alert("网络出现波动异常");
    	}
    
    	// 点击发送消息按钮
    	$('#submit').click(function () {
        	if (isEnd) {
            	alert("本次拍卖会以结束,请关注其他拍卖物品");
            	return false;
        	}
        	// 获取输入的内容
        	var data = $('#context_text').val();
        	var reg = /^[1-9]\d*$/;
        	if (!reg.test(data)) {
            	alert('请输入正确的数值');
            	return false;
        	}
        	var price = sessionStorage.getItem('price');
        	var auctionsById = null;
        	if (price==null || price==undefined) {
            	// 设置ajax请求同步
            	$.ajaxSettings.async = false;
            	// 缓存中没有消息,首次喊价,不能低于起拍价
            	var bool = false;
            	$.get("getStartPrice",{itemId:itemId},function (auctions) {
                	auctionsById = JSON.parse(auctions);
                	if (data<auctionsById.startPrice) {
                    	alert('不能低于起拍价:'+auctionsById.startPrice);
                    	bool = true;
                    	return false;
                	}
                	if (new Date().getTime() < auctionsById.startTime) {
                    	alert("拍卖未开始,请稍等");
                    	bool = true;
                    	return false;
                	}
                	if (new Date().getTime() > auctionsById.endTime) {
                    	alert("拍卖已结束!");
                    	bool = true;
                    	return false;
                	}
            	});
            	if (bool) {
                	return false;
            	}
            	// ajax请求结束,恢复其异步
            	$.ajaxSettings.async = true;
        	} else {
            	// 缓存中有,不能低于前一次喊价
            	if (data<=price) {
                	alert('请加价后再喊价');
                	return false;
            	}
            	/*if (new Date().getTime() > auctionsById.endTime) {
                	alert("拍卖已结束!");
                	bool = true;
                	return false;
            	}*/
        	}
        	// 获取到后清空输入区域
        	$('#context_text').val('');
        	// 将消息格式化
        	var json = {"message":data};
        	// 将消息展示在消息区域
        	var str = "<div class=\"msg guest\"><div class=\"msg-right\"><div class=\"msg-host headDefault\"></div><div class=\"msg-ball\">"+data+"</div></div></div>"
        	// 直接拼接在后面
        	$('#msgs').append(str);
        	// 禁止再次点击
        	$("#submit").css({"pointer-events": "none" });
        	$("#submit").attr('title','请等待他人喊价!!!');
        	isPointer = true;
        	// 发送数据给服务端
        	socket.send(JSON.stringify(json));
    	});
    
    	socket.onclose=function () {
    
    	}
    
    	// 退出
    	$('#close').click(function () {
        	if (confirm('是否退出会场?')) {
            	// 退出,检查该用户是否最后一个喊价
            	if (isPointer) {
                	alert("您是最后喊价用户,无法退出");
                	return false;
            	} else {
                	socket.close();
                	window.location.href='init';
            	}
        	}
    	});
    });
    
springboot实现
环境准备

服务器使用核心是@ServerEndpoint这个注解。这个注解是Javaee标准里的注解
如果是用传统方法使用外部tomcat发布项目,需要在pom文件中引入javaee标准即可使用

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>

如果是使用springboot的内置tomcat时,就不需要单独引入javaee-api了
spring-boot已经包含了,这里只有引入springboot的websocket功能

第一步:增加依赖

springboot的高级组件会自动引用基础的组件
像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter
所以不要重复引入

<!-- websocket依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
第二步:增加WebSocket配置启动类(可以发现与普通SSM项目一样)
@Configuration
public class WebSocketConfig {
	// 首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket
	// endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		return new ServerEndpointExporter();
	}
}
第三步:增加MyWebSocket类(就是SSM中的PointChat类)
  • 相关概念

    WebSocket是类似客户端服务端的形式(采用ws协议),那么下面的MyWebSocket 其实就相当于一个ws协议的Controller
    直接在MyWebSocket 类上面加上@ServerEndpoint(“/imserver/{userId}”) 、@Component注解启用即可,相当于是一个controller了(但是是单例的,每一个新的连接都会创建一个对象)
    需要注意的是在普通的SSM中该类不能添加@Component注解,否则不起作用

  • 相关注解

    @OnOpen 客户端连接时调用
    @onClose 客户端关闭连接时调用
    @onMessage接收消息等方法
    @OnError 错误时候调用

  • 相关属性对象

    Session 成功连接的客户端,需要通过它来给客户端发送数据

  • 相关代码
    //单例类
    @ServerEndpoint(value = "/websocket/{userId}")
    @Component
    public class MyWebSocket {
    	public MyWebSocket() {
        	System.out.println("实例化");
    	}
    
    	/**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/
    	private static ConcurrentHashMap<String,MyWebSocket> webSocketMap = new ConcurrentHashMap<>();
    
    	/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
    	private Session session;
    
    	/**接收userId*/
    	private String userId="";
    
    	/**
     	* 连接建立成功调用的方法*/
    	@OnOpen
    	public void onOpen(Session session,@PathParam("userId") String userId) {
        	this.userId=userId;//连接成功就把用户的id保存起来,为后续的关闭及发送消息使用
        	this.session=session;
    
        	System.out.println(userId+"用户连接服务器成功"+session.getId());
        	try {
            	sendMessage("连接成功");//发送给客户端
        	} catch (IOException e) {
        	}
    	}
    
    	/**
     	* 连接关闭调用的方法
     	*/
    	@OnClose
    	public void onClose() {
        	System.out.println(this.userId+"关闭了");
    	}
    
    	/**
     	* 客户端主动发送消息后调用的方法
     	*
     	* @param message 客户端发送过来的消息*/
    	@OnMessage
    	public void onMessage(String message, Session session) {
        	System.out.println(this.userId+"发送的信息:"+message);
    
    	}
    	@OnError
    	public void onError(Session session, Throwable error) {
        	System.out.println(this.userId+"错误");
        	error.printStackTrace();
    	}
    	/**
     	* 自己封装的方法  服务器发送信息给客户端
     	*/
    	public void sendMessage(String message) throws IOException {
        	this.session.getBasicRemote().sendText(message);
    	}
    
    }
    
第四步:增加页面
  • 主要对象

    var websocket=new WebSocket

  • 相关事件

    websocket.onerror 错误时执行
    websocket.onopen 与服务器连接成功执行
    websocket.onmessage 接收到服务器方式的消息执行
    websocket.onclose 连接关闭执行

  • 相关方法

    websocket.close(); 关闭连接
    websocket.send(message); 方式消息给服务器

  • 相关代码(与上面SSM一样,页面没有变化)
    <body>
    Welcome<br/>
    <input id="text" type="text" /><button onclick="send()">Send</button>    <button onclick="closeWebSocket()">Close</button>
    <div id="message">
    </div>
    
    <script src="js/jquery-1.4.2.js"></script>
    
    <script type="text/javascript">
    	var websocket = null;
    
    	//判断当前浏览器是否支持WebSocket
    	if('WebSocket' in window){
        	websocket = new WebSocket("ws://localhost:8080/websocket");
    	}
    	else{
        	alert('Not support websocket')
    	}
    
    	//连接发生错误的回调方法
    	websocket.onerror = function(){
        	setMessageInnerHTML("error");
    	};
    
    	//连接成功建立的回调方法
    	websocket.onopen = function(event){
        	setMessageInnerHTML("open");
    	}
    
    	//接收到消息的回调方法
    	websocket.onmessage = function(event){
        	setMessageInnerHTML(event.data);
    	}
    
    	//连接关闭的回调方法
    	websocket.onclose = function(){
        	setMessageInnerHTML("close");
    	}
    
    	//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    	window.onbeforeunload = function(){
        	websocket.close();
    	}
    
    	//将消息显示在网页上
    	function setMessageInnerHTML(innerHTML){
        	document.getElementById('message').innerHTML += innerHTML + '<br/>';
    	}
    
    	//关闭连接
    	function closeWebSocket(){
        	websocket.close();
    	}
    
    	//发送消息
    	function send(){
        	var message = document.getElementById('text').value;
        	websocket.send(message);
    	}
    </script>
    
    </body>
    

实现聊天(修改页面)

	<div>
    	我(<%=request.getParameter("id")%>)模拟动态好友列表<br>
    	<input type="radio" value="2" name="fid">刘德华(2)<br>
    	<input type="radio" value="3" name="fid">普京(3)<br>
    	<input type="radio" value="4" name="fid"> 特朗普(4)<br>
	</div>
	<script>
		if('WebSocket' in window){
			websocket = new WebSocket("ws://localhost:8080/websocket/"+<%=request.getParameter("id")%>); 
			//获取登陆用户的id(可以从session里面动态获取登陆用户的id)
		}
		//发送消息
		function send(){
    		var message = document.getElementById('text').value;//获取发送的信息
    		var fid=$('input[name="fid"]:checked ').val();//获取要发送给那个好友id
    		var jsonobj={"message":message,"fromid":fid};
    		websocket.send(JSON.stringify(jsonobj));
		}
	</script>

springboot整合Quartz

基本概念

什么是Quartz

在项⽬开发中,经常需要定时任务来帮助我们来做⼀些内容,⽐如定时派息、跑批对账、业务监控等。Spring Boot 体系中现在有两种⽅案可以选择,第⼀种是 Spring Boot 内置的⽅式简单注解就可以使⽤,当然如果需要更复杂的应⽤场景还是得 Quartz 上场,Quartz ⽬前是 Java 体系中最完善的定时⽅案

Quartz的优点
  • 丰富的 Job 操作 API;
  • ⽀持多种配置;
  • Spring Boot ⽆缝集成;
  • ⽀持持久化;
  • ⽀持集群;
  • Quartz 还⽀持开源,是⼀个功能丰富的开源作业调度库,可以集成到⼏乎任何 Java 应⽤程序中。
Quartz体系结构

4 个核⼼的概念 Job(任务)、JobDetail(任务信息)、Trigger(触发器)和
Scheduler(调度器) 。

Job(任务)

是⼀个接⼝,只定义⼀个⽅法 execute(JobExecutionContext context),在实现接⼝的execute ⽅法中编写所需要定时执⾏的 Job(任务),JobExecutionContext 类提供了调度应⽤的⼀些信息;Job 运⾏时的信息保存在 JobDataMap 实例中。

JobDetail(任务信息)

Quartz 每次调度 Job 时,都重新创建⼀个 Job 实例,因此它不接受⼀个 Job 的实例,相反它接收⼀个 Job 实现类(JobDetail,描述 Job 的实现类及其他相关的静态信息,如 Job 名字、描述、关联监听器等信息),以便运⾏时通过 newInstance() 的反射机制实例化 Job。

Trigger(触发器)

是⼀个类,描述触发 Job 执⾏的时间触发规则,主要有 SimpleTrigger 和 CronTrigger 这两个⼦类。当且仅当需调度⼀次或者以固定时间间隔周期执⾏调度,SimpleTrigger 是最适合的选择;⽽CronTrigger 则可以通过 Cron 表达式定义出各种复杂时间规则的调度⽅案:如⼯作⽇周⼀到周五的 15:00~ 16:00 执⾏调度等。

Scheduler(调度器)

调度器就相当于⼀个容器,装载着任务和触发器,该类是⼀个接⼝,代表⼀个 Quartz 的独⽴运⾏容器,Trigger 和 JobDetail 可以注册到 Scheduler 中,两者在 Scheduler 中拥有各⾃的组及名称,组及名称是 Scheduler 查找定位容器中某⼀对象的依据,Trigger 的组及名称必须唯⼀,JobDetail 的组和名称也必须唯⼀(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。Scheduler 定义了多个接⼝⽅法,允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail。
在这里插入图片描述

实现步骤

第一步:导入依赖
<!--quartz依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
第二步:定义一个job

先定义一个自定义的Job,继承于QuartzJobBean即可

public class SampleJob extends QuartzJobBean {

    private String name;
    public void setName(String name) {
        this.name = name;
    }
    @Override
    protected void executeInternal(JobExecutionContext context)
            throws JobExecutionException {
        System.out.println(String.format("Hello %s!", this.name));
    }
}
第三步:构建Quartz配置类

配置类上加@Configuration表示一个配置类,项目启动后会立即执行
下面的方法为创建任务信息和触发器等对象,方法上加入@Bean即可
该配置类用来配置任务信息和触发器,以及调度

@Configuration
public class SampleScheduler {

    @Bean
    public JobDetail sampleJobDetail() {
        //withIdentity定义 TriggerKey,也可以不设置,会⾃动⽣成⼀个独⼀⽆⼆的 TriggerKey ⽤来区分不同的 Trigger
        //usingJobData("name", "World定时器") 设置SampleJob属性对应的值
        return JobBuilder.newJob(SampleJob.class).withIdentity("sampleJob")
                .usingJobData("name", "World定时器").storeDurably().build();
    }
    @Bean
    public Trigger sampleJobTrigger() {
        //withIntervalInSeconds(10)每隔10秒钟执行一次
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(10).repeatForever();
        return TriggerBuilder.newTrigger().forJob(sampleJobDetail())
                .withIdentity("sampleTrigger").withSchedule(scheduleBuilder).build
                        ();
    }

}
第四步:启动测试

直接启动该springboot项目即可,配置类会自动启动,执行相关内容
下面是启动项目后,每隔一秒执行任务代码
在这里插入图片描述

Cron表达式设置复杂的启动方式

上面的sampleJobTrigger为简单的触发器,一般用于执行简单的触发条件
Quartz还提供了基于日历条件的触发器
使用时把上面的那个sampleJobTrigger方法换成下面这个

@Bean
public Trigger printTimeJobTrigger() {
    CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");
    return TriggerBuilder.newTrigger()
            .forJob(sampleJobDetail())//关联上述的JobDetail
            .withIdentity("quartzTaskService")//给Trigger起个名字
            .withSchedule(cronScheduleBuilder)
            .build();

}
Cron表达式

表达式使用参考

表达式格式

{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}

字段允许值允许的特殊字符
0-59, - * /
0-59, - * /
小时0-23, - * /
日期1-31, - * ? / L W C
月份1-12 或者JAN-DEC, - * /
星期1-7 或者SUN-SAT, - * ? / L C #
年(可为空)留空, 1970-2099, - * /
表达式举例

“0 0 12 * * ?” 每天中午12点触发
“0 15 10 ? * *” 每天上午10:15触发
“0 15 10 * * ?” 每天上午10:15触发
“0 15 10 * * ? *” 每天上午10:15触发
“0 15 10 * * ? 2005” 2005年的每天上午10:15触发
“0 * 14 * * ?” 在每天下午2点到下午2:59期间的每1分钟触发
“0 0/5 14 * * ?” 在每天下午2点到下午2:55期间的每5分钟触发
“0 0/5 14,18 * * ?” 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
“0 0-5 14 * * ?” 在每天下午2点到下午2:05期间的每1分钟触发
“0 10,44 14 ? 3 WED” 每年三月的星期三的下午2:10和2:44触发
“0 15 10 ? * MON-FRI” 周一至周五的上午10:15触发
“0 15 10 15 * ?” 每月15日上午10:15触发
“0 15 10 L * ?” 每月最后一日的上午10:15触发
“0 15 10 ? * 6L” 每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6L 2002-2005” 2002年至2005年的每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6#3” 每月的第三个星期五上午10:15触发

springboot整合redis

springboot整合Shiro

springboot整合ActiveMQ

Spring Data JPA

常用注释

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值