会下载到本地,以后要用的时候,直接解压就可以用了,不用每次去下载
Spring Boot的基础结构共三个文件:
spingboot建议的目录结果如下:
resources ⽬录下:
采用默认配置可以省去很多配置,当然也可以根据自己的喜欢来进行更改
@SpringBootApplication
是一个组合注解,用于快捷配置启动类
该注解等价于同时使用3个注解@EnableAutoConfiguration+@ComponentScan+@Configuration
默认值
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.6version>
<relativePath/>
parent>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
pom.xml文件中默认有两个模块:
@Controller
public class Demo01Controller {
@ResponseBody
@RequestMapping("/demo01")
public String demo01() {
return "我是按照ssm方式的controller";
}
}
@RestController
public class Demo02Controller {
@RequestMapping("/demo02")
public String demo02() {
return "使用spring boot 提供的注解创建的controller";
}
}
(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.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
configuration>
plugin>
/**
* @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容器中获取该处理类对象时,其属性值就是从配置中获取的值
jdbc.id=10
jdbc.name=你好
jdbc.pass=nihao
@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等
增加@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;
}
}
@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}是我们自己定义的
server.servlet.context-path=/dev
me.name=dev
me.pass=123
server.servlet.context-path=/test
me.name=test
me.pass=789
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;
}
}
不需要指定加载配置文件的位置,加载默认配置文件即可
指定加载的前缀就是多环境配置文件中属性的前缀,其他与前面一直
@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等模版引擎
<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-jasperartifactId>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>jstlartifactId>
dependency>
springboot不支持jsp,故无法直接创建web模块,需要手动创建普通目录
创建好webapp目录后在下面创建view目录用于放置jsp页面,或者创建WEB-INF放置jsp都可以
http://localhost:8080/a.jsp 可以访问
http://localhost:8080/view/c.jsp 可以访问
http://localhost:8080/WEB-INF/b.jsp 不能访问
#jsp访问配置
server.servlet.context-path=/abc
spring.profiles.active=test
spring.mvc.view.prefix=/view/
spring.mvc.view.suffix=.jsp
// 此处如果使用@RestController注解,返回的都是json,无法跳转到jsp
@Controller
public class ViewTest {
@RequestMapping("/index01")
public String index() {
return "index";
}
}
@RequestMapping("/index02")
public ModelAndView index02() {
ModelAndView mav = new ModelAndView();
mav.setViewName("index");
mav.addObject("user","USER");
return mav;
}
jsp中接收参数我是index页面
mav:${user}
页面正常访问,参数正常获取一般一个项目中不糊同时出现thymeleaf和jsp,所以导入thymeleaf依赖时先删除jsp的依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
# thymeleaf视图解析
# 先禁用thymeleaf缓存,一般开发或测试环境都是禁用的
spring.thymeleaf.cache=false
# 默认的解析前缀地址为resources下的templates下,所以可以不写前缀
spring.thymeleaf.prefix=classpath:/templates/
# 默认的解析后缀为.html,所以可以不写后缀
spring.thymeleaf.suffix=.html
# thymeleaf视图解析
# 先禁用thymeleaf缓存,一般开发或测试环境都是禁用的
spring.thymeleaf.cache=false
# 默认的解析前缀地址为resources下的templates下,所以可以不写前缀
# spring.thymeleaf.prefix=classpath:/templates/
# 默认的解析后缀为.html,所以可以不写后缀
# spring.thymeleaf.suffix=.html
# 自定义位置
spring.thymeleaf.prefix=/temp/
# 后缀可以写,可以不写,默认就是.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
aa.html是的<h1 th:text="${a}">Hello World 作用域必须有a,否则出错h1>
body>
@Controller
public class ThymeleafTest {
@RequestMapping("/thy01")
public ModelAndView thy01() {
ModelAndView mav = new ModelAndView();
mav.setViewName("index");
mav.addObject("user","USER");
return mav;
}
}
@RequestMapping("/thy02")
public String thy02() {
return "index";
}
此处与SSM不同,SSM中需要导入mybatis依赖,需要导入spring整合mybatis依赖,此处只需要导入一个springboot整合依赖即可
springboot整合mybatis依赖中将相关的依赖都进行了整合,只需要导入这一个依赖就可以了
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.3version>
dependency>
此处需要注意的是,导入依赖时不需要指定版本信息
如果默认的版本不能 使用,可以自行确定版本信息
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
数据源信息相关属性为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配置信息属性为整合依赖中提供
在导入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文件
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
public interface UserDao {
Users queryById(int id);
List<Users> queryAll();
}
@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注解的接口就不同
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>
public interface UserService {
Users queryById(int id);
List<Users> queryAll();
}
@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();
}
}
@RestController
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/getUser")
public List<Users> getUserById() {
return userService.queryAll();
}
}
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>5.2.1version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-autoconfigureartifactId>
<version>1.2.10version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.2.10version>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>2.1.5version>
dependency>
# 指定访问时项目名称
server.servlet.context-path=/abc
# 指定需要激活的配置文件
spring.profiles.active=test
spring.datasource.url=jdbc:mysql://localhost:3306/xdz2001
spring.datasource.username=root
spring.datasource.password=root
# 指定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执行的SQL语句,前面logging.levvel是固定的
# 后面com.example.demo.dao是指定mybatis执行的dao接口包
# DEBUG指的是打印调试相关的东西,包含SQL语句等
logging.level.com.example.demo.dao=DEBUG
########## 通用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
创建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文件中也不需要添加东西,除非是自己定义的方法
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;
}
@RequestMapping("/hello3")
public List<User> index3() {
List<User> list = userService.selectAll();
System.out.println("ss");
return list;
}
: ==> Preparing: SELECT id, name, address,sex,love,imgpath FROM user2
: ==> Parameters:
: <==Total : 257
代码
@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中 配置事务需要两步操作
在启动类中添加注解,启动事务管理
@SpringBootApplication
@MapperScan("com.example.demo.dao")
@EnableTransactionManagement
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
在具体需要事务 管理的方法上添加事务,该方法将是一个事务
注解中可以有参数指定,比如指定回滚
还可以指定该方法为只读方法等
当注解没有参数时,可以带有括号也可以没有,都是可以的
该注解还可以用在类上面,该类中所有方法都被事务管理
@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();
}
}
1.已注解了@Transactional的事务仍会有“出现异常事务不回滚”的情况?例如mybatis的xml配置标签错误时,运行报异常,但仍然能够进行增加操作。
2.Java阿里巴巴规范提示,事务需要进行手动回滚。为什么?
Spring框架的事务管理默认地只在发生不受控异常(RuntimeException和Error)时才进行事务回滚。也就是说,当事务方法抛出受控异常(Exception中除了RuntimeException及其子类以外的)时不会进行事务回滚。
@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依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
# thymeleaf依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
<version>2.0.4.RELEASEversion>
dependency>
# jsp依赖
<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-jasperartifactId>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>jstlartifactId>
dependency>
# 是否支持批量上传 (默认值 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
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (20738021) exceeds the configured maximum (10485760)
<%@page isELIgnored="false" language="java" contentType="text/html; utf-8" pageEncoding="UTF-8" %>
单一文件上传示例
批量文件上传示例
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;
}
}
在前面我们学习了springboot上传文件
通过学习我们可以实现文件上传,但是依然有问题存在
在springboot运行时,我们的项目运行在一台服务器上
项目运行的服务器,也就是我们的tomcat服务器,我们可以认为就是web服务器
而一般实际开发中,几乎没有将上传文件保存在web服务器中的
而要将web服务器上传的文件保存到其他数据服务器上,这里我们就借助FastDFS实现
此时我们可以将FastDFS看做文件服务器,将web上传的文件保存到上面去
开源的文件服务器有很多,目前中小型企业用的比较多的就是FastDFS
所以我们在开始项目之前应该先将FastDFS服务器搭建好
/usr/bin/fdfs_stackerd /etc/fdfs/tracker.conf restart
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
/usr/bin/fdfs_test /etc/fdfs/client.conf upload /home/aaa.png
<dependency>
<groupId>com.github.tobatogroupId>
<artifactId>fastdfs-clientartifactId>
<version>1.26.5version>
dependency>
#连接超时时间
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中注入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());
}
}
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poi-ooxmlartifactId>
<version>3.14version>
dependency>
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poi-scratchpadartifactId>
<version>3.14version>
dependency>
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poi-excelantartifactId>
<version>3.14version>
dependency>
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poi-ooxml-schemasartifactId>
<version>3.14version>
dependency>
此处的工具类官方提供了读取各种格式文件的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测试,但是所有的数据都是写死的
@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;
}
}
实际项目开发中,程序往往会发生各式各样的异常情况,特别是身为服务端开发人员的我们,总是不停的编写接口提供给前端调用,分工协作的情况下,避免不了异常的发生
如果直接将错误的信息直接暴露给用户,这样的体验可想而知,且对黑客而言,详细异常信息往往会提供非常大的帮助
手动捕获异常信息,然后返回对应的结果集
相信很多人都看到过类似的代码(如:封装成Result对象);
该方法虽然间接性的解决错误暴露的问题,同样的弊端也很明显,增加了大量的代码量,当异常过多的情况下对应的catch层愈发的多了起来,很难管理这些业务异常和错误码之间的匹配
所以最好的方法就是通过简单配置全局掌控
@RequestMapping("/myexception1")
@ResponseBody
public String myexception1(int jsp) {
try{
return "json数据"
}catch(Exception e){
return "错误";
}
}
所谓全局异常,是指所有访问的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两个属性,可以当做客户端调用的依据
**
* 操作消息提醒
*
* @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);
}
}
在Restful风格中,用户请求的url使用同一个url而用请求方式:get,post,delete,put…等方式对请求的处理方法进行区分,这样可以在前后台分离式的开发中使得前端开发人员不会对请求的资源地址产生混淆和大量的检查方法名的麻烦,形成一个统一的接口。
在RestFul风格中,有如下规定:
访问同一个资源,提交请求的方式不一样,执行的操作也不一样
如当前url是 http://localhost:8080/User
那么用户只要请求这样同一个URL就可以实现不同的增删改查操作,例如
@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)
// 采用占位符方式传入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是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。
是由NetScape提出的一个著名的安全策略。
源(origin)就是协议、域名和端口号,所谓的同源,指的是协议,域名,端口相同。
浏览器处于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读写对方的资源。
http://localhost:8080/user
同源政策规定,AJAX请求只能发给同源的网址,否则就报错,有三种方法规避这个限制:
JSONP
WebSocket
CORS
@RestController
public class CorsController{
@RequestMapping("/test1")
public String test1(int a){
return "aa";
}
}
<%@page language="java" pageEncode="UTF-8" contentType="text/html;utf-8" %>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>htmlhtml>
<script src="/js/jquery-1.4.2.js">script>
<script>
function subcors() {
$.get("http://localhost:8080/test1",function (data){
alert(data);
});
}
script>
head>
<body>
<input type="button" value="访问同源资源" onclick="subcors()">
body>
html>
<%@page language="java" pageEncode="UTF-8" contentType="text/html;utf-8" %>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>htmlhtml>
<script src="/js/jquery-1.4.2.js">script>
<script>
function subcors() {
$.get("http://localhost:8080/test1",function (data){
alert(data);
});
}
script>
head>
<body>
<input type="button" value="访问同源资源" onclick="subcors()">
body>
html>
@CrossOrigin注解解决跨域问题
只需要在原来的controller上面加上该注解即可
注解的值里面的星号*表示允许任何源请求访问,也可以指定特定的请求访问,该值是数组形式,可以指定多个值
@RequestMapping("/test1")
@CrossOrigin(origins = "*") //允许不同源ajax请求
public String test1(int a){
return "aa";
}
在 Servlet/Jsp 项目中,如果涉及到系统任务,例如在项目启动阶段要做一些数据初始化操作,这些操作有一个共同的特点,只在项目启动时进行,以后都不再执行,这里,容易想到web基础中的三大组件( Servlet、Filter、Listener )之一 Listener ,这种情况下,一般定义一个 ServletContextListener,然后就可以监听到项目启动和销毁,进而做出相应的数据初始化和销毁操作
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
//在这里做数据初始化操作
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
//在这里做数据备份操作
}
}
这是基础 web 项目的解决方案,这种方式在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>htmlhtml>
head>
<body>
${applicationScope.abc}
body>
html>
Swagger 是⼀系列 RESTful API 的⼯具,通过 Swagger 可以获得项⽬的⼀种交互式⽂档,客户端 SDK 的⾃
动⽣成等功能。
通过扫描代码去生成描述文件,连描述文件都不需要再去维护了。所有的信息,都在代码里面了。代码即接口文档,接口文档即代码
通俗的讲,就是生成项目相关的接口使用手册
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.8.0version>
dependency>
// @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/", "[email protected]"))
.build();
}
}
Swagger 通过注解表明该接⼝会⽣成⽂档,包括接⼝名、请求⽅法、参数、返回信息等,常⽤注解内容如下
作用范围 | API | 使用位置 |
---|---|---|
协议集描述 | @API | 用于Controller类上 |
协议描述 | @APIOperation | 用于Controller方法上 |
非对象参数集 | @ApiImplicitParams | 用于Controller方法上 |
非对象参数描述 | @ApiImplicitParam | 用在@APIImplicitParams的方法里面 |
响应集 | @ApiResponses | 用于Controller的方法上 |
响应信息参数 | @ApiResponse | 用在@ApiResponses 的方法里面 |
描述返回对象的意义 | @ApiModel | 用在返回对象上面 |
对象属性 | @ApiModelProperty | 用在出入参数对象的字段上 |
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 {
把不同的controller里面的方法按照tags组合在一组,和@Api在同一级
@GetMapping("/a")
@ApiOperation(value="获取用户信息",tags={"获取用户信息copy"},notes="注意问题点")
public void a(){
}
可以包含多个 @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) {
}
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-websocketartifactId>
<version>5.3.8version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-messagingartifactId>
<version>5.3.8version>
dependency>
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter getServerEndpointExporter() {
return new ServerEndpointExporter();
}
}
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;
}
}
@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);
}
}
/**
* 用来管理连接
* 该类的每个对象代表客户端的一个连接,如两人聊天,就有两个该对象
*
* @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();
}
}
/**
* 线程类,用于判断落锤
*
* @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>
<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/[email protected]/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">×button>div>
<div id="chatMes" style="text-align: center;color: #6fbdf3;font-family: 新宋体">
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>
<script>
/*https://unpkg.com/[email protected]/assets/shizuku.model.json*/
L2Dwidget.init({ "model": { jsonPath:
"https://unpkg.com/[email protected]/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>
此处样式文件代码较多,可从gitee远程仓库项目中下载使用
var toName;
var isEnd = false;
var isPointer = false;
function showChat(name) {
toName = name;
//清除聊天区的数据
$("#msgs").html("");
//现在聊天对话框
$("#chatArea").css("display","inline");
//显示“正在和谁聊天”
$("#chatMes").html("正在和 "+toName+" 聊天");
}
$(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 = ""+res.message+"";
$('#msgs').append(str);
}
$.get("getStartPrice",{itemId:itemId},function (auctions) {
var auctionsId = JSON.parse(auctions);
$("#chatMes").html(""+auctionsId.auctionItem.name+"起拍价:"+auctionsId.startPrice+"");
});
}
// 接收到服务端发送的数据后触发
socket.onmessage=function (event) {
// 带有事件参数,通过该事件对象获取服务端发送的数据
var res = JSON.parse(event.data);
if (res.isSystem) {
if (res.fromName.indexOf('all')==0) {
$('#broadcastList').html("- 恭喜"
+res.fromName.substring(3)+"以"+res.message+"价格拍得
服务器使用核心是@ServerEndpoint这个注解。这个注解是Javaee标准里的注解
如果是用传统方法使用外部tomcat发布项目,需要在pom文件中引入javaee标准即可使用
<dependency>
<groupId>javaxgroupId>
<artifactId>javaee-apiartifactId>
<version>7.0version>
<scope>providedscope>
dependency>
如果是使用springboot的内置tomcat时,就不需要单独引入javaee-api了
spring-boot已经包含了,这里只有引入springboot的websocket功能
springboot的高级组件会自动引用基础的组件
像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter
所以不要重复引入
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
@Configuration
public class WebSocketConfig {
// 首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket
// endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
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); 方式消息给服务器
<body>
Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Sendbutton> <button onclick="closeWebSocket()">Closebutton>
<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 + '
';
}
//关闭连接
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>
在项⽬开发中,经常需要定时任务来帮助我们来做⼀些内容,⽐如定时派息、跑批对账、业务监控等。Spring Boot 体系中现在有两种⽅案可以选择,第⼀种是 Spring Boot 内置的⽅式简单注解就可以使⽤,当然如果需要更复杂的应⽤场景还是得 Quartz 上场,Quartz ⽬前是 Java 体系中最完善的定时⽅案
4 个核⼼的概念 Job(任务)、JobDetail(任务信息)、Trigger(触发器)和
Scheduler(调度器) 。
是⼀个接⼝,只定义⼀个⽅法 execute(JobExecutionContext context),在实现接⼝的execute ⽅法中编写所需要定时执⾏的 Job(任务),JobExecutionContext 类提供了调度应⽤的⼀些信息;Job 运⾏时的信息保存在 JobDataMap 实例中。
Quartz 每次调度 Job 时,都重新创建⼀个 Job 实例,因此它不接受⼀个 Job 的实例,相反它接收⼀个 Job 实现类(JobDetail,描述 Job 的实现类及其他相关的静态信息,如 Job 名字、描述、关联监听器等信息),以便运⾏时通过 newInstance() 的反射机制实例化 Job。
是⼀个类,描述触发 Job 执⾏的时间触发规则,主要有 SimpleTrigger 和 CronTrigger 这两个⼦类。当且仅当需调度⼀次或者以固定时间间隔周期执⾏调度,SimpleTrigger 是最适合的选择;⽽CronTrigger 则可以通过 Cron 表达式定义出各种复杂时间规则的调度⽅案:如⼯作⽇周⼀到周五的 15:00~ 16:00 执⾏调度等。
调度器就相当于⼀个容器,装载着任务和触发器,该类是⼀个接⼝,代表⼀个 Quartz 的独⽴运⾏容器,Trigger 和 JobDetail 可以注册到 Scheduler 中,两者在 Scheduler 中拥有各⾃的组及名称,组及名称是 Scheduler 查找定位容器中某⼀对象的依据,Trigger 的组及名称必须唯⼀,JobDetail 的组和名称也必须唯⼀(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。Scheduler 定义了多个接⼝⽅法,允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-quartzartifactId>
dependency>
先定义一个自定义的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));
}
}
配置类上加@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
();
}
}
上面的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();
}
表达式使用参考
{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒 | 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触发