JAVAweb案例之后端的增删改查
一.知识点
1.日志
在controller层以及其他层中,每一个方法输出日志的System.out.println显的不够专业。
那么如何显得专业呢?
使用 log back框架记录日志,首先先生成一个记录对象:
import java.util.logging.Logger; @RestController public class Deptcontroller {private static Logger log = (Logger) LoggerFactory.getLogger(Deptcontroller.class);// 日志,用来代替sout(),这样显得规范
但其实这代码很长一段而且很难记,于是我们现在已经使用注解:@Slf4j 来进行代替。
2.接口请求
众所周知,对一个接口我们可以进行get,post,delete等多种请求方式。
那么如果我只想让get这个请求方式可以成功,其他不可以应该如何做到?
使用method属性:
在@RequestMappering注解中有这么一个属性method就是用来指定请求方式:
@RequestMapping(value = "/dept",method = RequestMethod.GET)
但是这样写的话其实会有一点点繁琐。好在有一个requestmappering的衍生注解:
@GetMapping("/dept")
如果是post,则是PostMappering,其他同理。
3.简化路径
如图所示,发现我们在每一个功能上,都写了同一个路径。为了简化请求路径的定义,可以将公共部分请求路径直接抽取到类上:
4.文件上传之from标签的属性
<!--action代表这个表单往哪提交,method指定表单提交方式为psot,enctype设置表单的编码格式-->
<form action="/upload" method="post" enctype="multipart/form-data">
5.服务端接收上传的文件
在spring中有一个api叫做MultipartFile,通过这个api就可以接收上传上来的文件。
如果要保证能够成功上传,那么表单项的名称必须要和方法形参名称一致。
如果不一致,则是可以通过注解@RequestParam进行绑定。
注意,在controller层中@PostMappering注解是写上传的路径的,因为上传路径是post请求
6.配置文件
1.参数配置化
在做项目时,每涉及到一个第三方技术或者服务,就将其参数硬编码在java代码中会出现问题:
1.如果参数发生变化,我们需要改动这些参数然后需要重新进行代码编译,这是比较繁琐的。
2.一个项目会有很多java类,如果将这些参数分散的定义在各个类中,我们如果需要修改参数值就需要一个一个定位。
如何将这些参数集中管理?
那就是放入到配置文件中:
正常做法是将配置文件中配置的属性值读取出来并分别赋值给ALIOSSUtils这个工具类,想要这样有一个注解可以实现要使用IO流。通过io流来读取配置文件然后解析配置项的值,然后给成员变量赋值,但有一个简单的方法:属性注入:@Value
@value注解通常用于外部配置的注入。
2.yml配置文件
properties配置文件的格式为:key=value
此外还有一种配置文件,它的格式可以为yml,也可以为yaml
yml配置文件是按照层级配置的。
注意的是第二级相对第一级前面是有缩进的。
常见配置文件对比:
yml格式的基本语法
yml常见数据格式:
3.@ConfigurationProperties注解
如果我们要注解的属性值比较多,几十个几百个,我们得写一堆value注解,此时这就比较繁琐臃肿。
所以spring提供了一种简化方式,可以自动的将配置项的值注入到对象的属性中。
但是这是有前提条件的:
1.配置文件中key的名字必须要和实体类中的属性名一致。并且需要提供getter/setter方法
2.需要将这个类交给IOC容器管理,成为bean对象,所以需要记得加上一个注解@Component.
此时还不能完成自动注入,因为目前仅仅是bean对象的属性名与配置项的后缀保持一致,还有个前缀aliyun.oss需要解决。
所以需要指定前缀:
二.案例
1.部门管理-查询
1.查询部门
流程:前端发送请求之后请求回到controller的这个方法:
@GetMapping("/dept")public Result list(){ log.info("查询所有部门"); //调用service查询部门数据return Result.success();
在controller方法当中,首先调用sqervice来获取数据
@Autowiredprivate DeptService deptService;// @GetMapping("/dept")// public Result list(){ //log.info("查询所有部门"); //调用service查询部门数据List<Dept> list = deptService.list();return Result.success();
在service中由掉用了mapper接口中的方法来查询全部部门信息
@Service public class DeptServiceImpl implements DeptService {@Autowiredprivate Deptmapper deptmapper;@Overridepublic List<Dept> list() {return deptmapper.list();
那mapper接口就会往数据库发送sql语句查询全部部门,并将查询的部门信息封装在List的集合中。最终将这个集合的数据返回给service,service又返回给controller,controller给前端。
@Mapper public interface Deptmapper {//查询全部部门数据@Select("select * from dept")List<Dept> list(); }
查询部门的完整代码(除实体类):
@RestController @Slf4j public class Deptcontroller {// private static Logger log = (Logger) LoggerFactory.getLogger(Deptcontroller.class);// 日志,用来代替sout(),这样显得规范//但是以上代码太过繁琐,所以可以使用@S1f4j // @RequestMapping(value = "/dept",method = RequestMethod.GET)@Autowiredprivate DeptService deptService;@GetMapping("/dept")public Result list(){ log.info("查询所有部门"); //调用service查询部门数据List<Dept> list = deptService.list();return Result.success(list);}
public interface DeptService { // 查询所有部门 List<Dept> list(); }
@Service public class DeptServiceImpl implements DeptService {@Autowiredprivate Deptmapper deptmapper;@Overridepublic List<Dept> list() {return deptmapper.list();} }
@Mapper public interface Deptmapper {@Select("select * from dept")List<Dept> list();//查询全部部门数据}
2.前后端联调
使用nginx.
3.删除部门
congtroller层:
@DeleteMapping("/dept/{id}") public Result delete(@PathVariable Integer id){log.info("根据id删除部门:{}",id);//调用service删除部门数据deptService.delete(id);return Result.success(); }
service层:
//删除需要部门void delete(Integer id); }
serviceImpl层:
@Override public void delete(Integer id) {deptmapper.delete(id); }
mapper层:
//根据id删除@Delete("delete from dept where id=#{id}")void delete(Integer id);//查询全部部门数据
4.新增部门
一般来说,新增的格式是json格式。也就是说,前端在请求这个接口时,这个表单数据将会以json格式提交到服务端。服务端接收的也是json格式数据。
响应数据也是json,但是统一响应结果。
前端传递的json格式参数如何在服务端接收?
可以通过一个实体类来接收,但需要在实体类的形参前加上一个注解@Requestbody,通过这个注解就可以将json格式数据封装到实体类。
//添加接口@PostMapping("/dept")public Result add(@RequestBody Dept dept){log.info("添加部门:{}",dept);//调用方法deptService.add(dept);return Result.success();}
//添加部门void add(Dept dept);
@Override public void add(Dept dept) {//补全属性dept.setCreateTime(LocalDateTime.now());dept.setUpdateTime(LocalDateTime.now());deptmapper.add(dept); }
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")void add(Dept dept);
2.员工管理
1.分页查询
在mysql数据库,分页查询的语法是这样的:
这样的功能不需要在服务端实现,只需要在前端构建。
这样的功能需要服务端查询并返回。
如何获取:使用聚合函数
、
像这样的总页数不需要服务端查询返回因为前端只要拿到了总记录数就可以自己计算出有多少页。
像这样一个分页查询虽然有较多功能,但只有打勾部分需要在服务端实现。
分析:
因为数据列表有多条记录,所以封装的是一个list集合。而总记录数就是一个数字。
服务端给前端响应的数据最终就是我们设置的controller方法的返回值,而一个方法只有一个返回值。
现在要给前端返回两个类型不一样的数据,那么应该如何返回呢?
可以将两个数据封装起来,考虑使用map集合或者是实体类进行封装。
所以,在进行分页查询前,我们还需要定义一个实体类来进行封装:
在进行分页时,三层架构的代码:
需求:
这是正常想法:
@GetMapping("/emp")public Result page(Integer page, Integer pageSize){if (page == null) page=1;if (pageSize == null) pageSize=10;return Result.success();}
但缺点是较为繁琐,可以试着使用注解完成如果未指定那么默认值为x的问题。
那就是@RequestParam,而设置默认值的属性为default value。
代码:
@Slf4j @RestController public class Empcontroller {@Autowiredprivate EmpService empService; @GetMapping("/emp")public Result page(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize){ log.info("分页查询员工,参数:page={},pageSize={}",page,pageSize);//{}里面的内容会由后面的形参自动填充PageBean pageBean = empService.page(page,pageSize);return Result.success(pageBean);} }
public interface EmpService {PageBean page(Integer page, Integer pageSize); }
@Service public class EmpServiceImpl implements EmpService {@Autowiredprivate Empmapper empmapper;@Overridepublic PageBean page(Integer page, Integer pageSize) { //1.获取总记录数Long total = empmapper.count();//2.获取分页查询结果列表Integer start = (page-1)*pageSize;List<Emp> rows = empmapper.page(start,pageSize);//3.封装PageBean对象并返回PageBean pageBean = new PageBean(total,rows);return pageBean;} }
@Mapper public interface Empmapper {/*** 查询所有员工* @return*/ @Select("select count(*) from emp")public long count(); /*** 分页查询员工* @return*/ @Select("select * from emp limit #{start},#{pageSize}") public List<Emp> page(@Param("start") Integer start,@Param("pageSize") Integer pageSize);//要加上@Param注解:使用@Param注解标记参数名,便于MyBatis在SQL映射中通过参数名引用这些值 }
其实以上操作略微繁琐,可以使用一个插件:PageHelper来帮助简化流程。
简化后的代码(只需要更改mapper层与serviceimpl层)
/* 员工信息查询*/ @Select("select * from emp")public List<Emp> list();}
@Override public PageBean page(Integer page, Integer pageSize) {//1.设置分页参数PageHelper.startPage(page,pageSize);//2.执行查询List<Emp> rows =empmapper.list();//现在获取到的这个list集合其实是分页查询结果的封装类,还要强转为page类型Page<Emp> p = (Page<Emp>) rows;//这是强转,page是插件自带的方法//3.封装PageBean对象并返回PageBean pageBean = new PageBean(p.getTotal(),p.getResult());return pageBean; }
2.带条件的分页查询
控制层:接收页面传递过来的请求参数,因为是根据姓,性别,修改日期创建日期进行查询,所以还需要添加四个查询条件。
//@DateTimeFormat注解:将前台传递的时间字符串转换成LocalDateTime对象public Result page(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize,String name,Short gender,@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDate begin ,@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDate end){ log.info("分页查询员工,参数:page={},pageSize={},{},{},{},{}",page,pageSize,name,gender,begin,end);//{}里面的内容会由后面的形参自动填充PageBean pageBean = empService.page(page,pageSize,name,gender,begin,end);return Result.success(pageBean);} }
而controller又需要将这四个参数传递给service:
public interface EmpService {PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end); }
需要实现类进行试补充实现:
@Override public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {//1.设置分页参数PageHelper.startPage(page,pageSize);//2.执行查询List<Emp> rows =empmapper.list(name,gender,begin,end);//现在获取到的这个list集合其实是分页查询结果的封装类,还要强转为page类型Page<Emp> p = (Page<Emp>) rows;//这是强转,page是插件自带的方法//3.封装PageBean对象并返回PageBean pageBean = new PageBean(p.getTotal(),p.getResult());return pageBean; }
在调用mapper接口的list方法时要满足将这四个参数再传递给map接口,mapper接口再根据传递进来的条件进行查询。
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);}
由于是条件查询又是动态sql,因此sql语句最好写在xml文件中:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.Empmapper"> <select id="list" resultType="com.example.pojo.Emp">select * from emp<where><if test="name != null">name like concat('%',#{name},'%')</if><if test="gender != null">and gender=#{gender}</if><if test="begin != null and end != null">and entrydate between #{begin} and #{end}</if></where>order by update_time desc </select> </mapper>
3.删除员工
一般来说,删除与批量删除只需要创建一个批量删除的接口即可。
controller:
//批量删除 @DeleteMapping("/emp/{ids}") public Result delete(@PathVariable List< Integer> ids){ log.info("批量删除员工,参数:{}",ids); empService.delete(ids); return Result.success();
service:
//批量删除void delete(List<Integer> ids);
serviceimpl:
@Override public void delete(List<Integer> ids) {empmapper.delete(ids); }
mapper:
//批量删除void delete(List<Integer> ids);
mapper.xml:
<!--批量删除,item可自定义,collertion不可以--><delete id="delete">delete from emp where id in<foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach></delete>
4.新增员工
@PostMapping("emp") public Result save(@RequestBody Emp emp){ log.info("保存员工,参数:{}",emp); empService.save(emp); return Result.success();
void save(Emp emp);
@Override public void save(Emp emp) {emp.setCreateTime(LocalDate.now().atStartOfDay());emp.setUpdateTime(LocalDate.now().atStartOfDay());empmapper.insert(emp);
@Insert("insert into emp(username,password,name,gender,image,job,entrydate,dept_id,create_time,update_time) values(#{username},#{password},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})") void insert(Emp emp);
5.修改员工
修改员工这个其实得分成两步进行:
1.先根据id查询出要修改的数据用于页面的回显展示。
2.点击保存之后需要更新数据库数据
先进行第一步查询回显:
代码:
//修改员工 //第一步查询回显 @GetMapping("/emp/{id}") public Result getbyid(@PathVariable Integer id){ log.info("回显员工,参数:{}",id); Emp emp = empService.getbyid(id); return Result.success(emp);}
Emp getbyid(Integer id);
@Override public Emp getbyid(Integer id) {return empmapper.getbyid(id); }
@Select("select * from emp where id = #{id}")Emp getbyid(Integer id);
然后第二步修改员工:
@PutMapping("/emp") public Result update(@RequestBody Emp emp){ log.info("修改员工,参数:{}",emp); empService.update(emp); return Result.success(); }
@Override public void update(Emp emp) { emp.setUpdateTime(LocalDateTime.now()); empmapper.update(emp);}
//更新员工void update(Emp emp);
<!-- 更新员工--><update id="update">update emp<set><if test="username != null">username=#{username},</if><if test="password != null">name=#{name},</if><if test="password != null">password=#{password},</if> <if test="name != null">name=#{name},</if><if test="gender != null">gender=#{gender},</if><if test="image != null">image=#{image},</if><if test="job != null">job=#{job},</if><if test="entrydate != null">entrydate=#{entrydate},</if><if test="deptId != null">dept_id=#{deptId},</if><if test="createTime != null">create_time=#{createTime},</if></set> where id=#{id}</update>
修改使用put请求。
其实发现,如果想要修改的话,前端页面传递过来的其实是一个json格式的请求参数,在服务端就需要一个实体类来接收,并且需要一个注解。
3.文件上传
1.简介
前端的文件上传:
服务端接收文件:
只写一个控制层:
@Slf4j @RestController public class UploadController {@PostMapping("/upload")public Result upload(String username, Integer age, MultipartFile image){log.info("文件上传:{},{},{}",username,age,image); return Result.success();} }
文件上传后会存储在c盘的一个目录
会有三个文件,因为我们提交的表单项有三个,分别是username,age,image.
此外,这三个文件都是临时文件。
只要文件上传这次请求响应完毕之后,临时文件会被自动删除。
所以还得有一个文件保存功能。
2.本地存储
在服务端,接收到上传的文件后,将文件存储到本地服务器磁盘中。
为什么代码本地存储不将文件名直接写死,如果将文件名固定了将来这个文件夹下只有这么一个文件,应该自己设定。而且,有一个原则,那就是页面传递的文件夹叫什么存储到服务器端也得叫什么。
//将接收到的文件存储到服务器的磁盘目录:E:\IDEA\自学java\javaweb\springboot-anli\src\main\resources\upload//如何在服务端获取上传文件的文件名:String originalFilename = image.getOriginalFilename();//而有一个现成的方法:teansferToimage.transferTo(new File("E:\\IDEA\\自学java\\javaweb\\springboot-anli\\src\\main\\resources\\upload\\"+originalFilename));return Result.success();}
注意, image.getOriginalFilename();这个调用是用来找到上传文件的原始文件名,然后好放在下文的本地存储作为文件名。并且注意,在resources\\upload\\中,一定要在upload后加上“\\”。
但是如果这样使用原始文件名上传,也会出现问题,比如说不同人上传同一个名字的文件,如果这样上传会造成覆盖。
为了防止这样的事故,我们就得给文件名不同的值,这样就得提到一个东西:UUID。
UUID(通用唯一识别码,简单来说就是一个长度固定的不会重复的字符串)
@Testvoid testUUID(){for (int i=0;i<1000;i++){String uuid = UUID.randomUUID().toString();System.out.println(uuid);} }
这就是UUID的样子。但是没有后缀,得拼接上文件的拓展名。
如何拿文件的拓展名:lastIndexOf方法可以实现
//构建唯一的文件名(不能重复):UUID(通用唯一识别码,简单来说就是一个长度固定的不会重复的字符串)//获取的UUID没有后缀,所以得将原始文件名的后缀提取 int index = originalFilename.lastIndexOf("."); originalFilename.substring(index); String nextDileName = UUID.randomUUID().toString()+index; log.info("新文件名:{}"+nextDileName);
完整的本地存储的控制层代码(只需要写控制层)
@Slf4j @RestController public class UploadController {@PostMapping("/upload")public Result upload(String username, Integer age, MultipartFile image) throws IOException {log.info("文件上传:{},{},{}",username,age,image); //将接收到的文件存储到服务器的磁盘目录://如何在服务端获取上传文件的原始文件名:String originalFilename = image.getOriginalFilename();//构建唯一的文件名(不能重复):UUID(通用唯一识别码,简单来说就是一个长度固定的不会重复的字符串)//获取的UUID没有后缀,所以得将原始文件名的后缀提取int index = originalFilename.lastIndexOf(".");originalFilename.substring(index);String nextDileName = UUID.randomUUID().toString()+index;log.info("新文件名:{}"+nextDileName);//而有一个现成的方法:teansferToimage.transferTo(new File("E:\\IDEA\\自学java\\javaweb\\springboot-anli\\src\\main\\resources\\upload\\"+nextDileName));return Result.success();}
但注意,上传文件发现大小超过1m后就报错,报错信息500是服务端异常。
因为上传文件过大超出限制。默认情况上传大小就为1m,超出即报错。
但是可以自行配置:
注意:在项目开发本地存储其实很少用。因为上传的文件全部存储在磁盘目录的话其实在前端是无法直接访问的。并且都存储在本地磁盘会导致容量不够