持续学习&持续更新中…

守破离


【Java从零到架构师第③季】【项目实战】驾考管理系统

  • 企业开发中常见的后台管理系统
  • 技术栈及第三方库
    • 技术栈
    • pom.xml
  • 各种Object
  • 前后端分离
    • 前后端分离—实现
    • 前后端分离—约定数据格式
  • 跨域问题
  • Layui
  • MySQL建议
  • 数据字典
  • 封装MyBatis-Plus方便查询
  • 封装给客户端的返回值
  • 统一异常处理+HTTP响应状态码
  • 统一异常处理—配合Shiro
  • 数据的一致性
  • 拼音库—tinypinyin的使用
  • MapStruct
  • 登录—简单登录
  • 登录—Token
    • 后端
    • 前端
  • 权限管理—RBAC
  • 逻辑删除
  • 逻辑删除—MyBatisPlus
  • 企业级文件上传
  • @RequestBody修饰的请求参数
  • 单元测试
    • Spring单元测试
    • SpringBoot单元测试
  • 打包部署
    • 打包部署—jar
    • 打包部署—war
  • 注意
  • 补充:ChromeJSON插件
  • 参考

【Github】项目源码地址:https://github.com/lpruoyu/JAVAEE_PROJECT_jiakao

企业开发中常见的后台管理系统

技术栈及第三方库

技术栈

后台:

  • SpringBoot
  • MyBatisPlus、Druid
  • 权限控制:Shiro
  • 后端校验:HibernateValidator
  • 验证码:easy-captcha
  • 缓存:ehcache
  • 对象转换:MapStruct
  • 拼音相关:tinypinyin
  • 接口文档:Swagger
  • 代码生成:EasyCode

前端:

  • layui(LayuiMini)
  • md5加密
  • Freemarker(项目初期使用,前后端分离后就不用了)
  • font-awesome

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>programmer.lp.jkbe_v3</groupId><artifactId>JiaKaoBE_V3</artifactId>
<!--    <packaging>war</packaging>--><packaging>jar</packaging><version>1.0.0</version><parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.boot</groupId><version>2.3.4.RELEASE</version></parent><properties><druid.version>1.2.1</druid.version><mybatis.plus.version>3.4.1</mybatis.plus.version><tinypinyin.version>2.0.3</tinypinyin.version><mapStruct.version>1.4.1.Final</mapStruct.version><captcha.version>1.6.2</captcha.version><commons.io.version>2.11.0</commons.io.version><swagger.models.version>1.6.2</swagger.models.version><swagger.triui.version>1.9.6</swagger.triui.version><swagger.version>2.9.2</swagger.version><shiro.version>1.7.0</shiro.version><springfox.version>3.0.0</springfox.version></properties><dependencies>
<!--        <dependency>-->
<!--            <groupId>javax.servlet</groupId>-->
<!--            <artifactId>javax.servlet-api</artifactId>-->
<!--            <scope>provided</scope>-->
<!--        </dependency>--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
<!--            <exclusions>-->
<!--                <exclusion>-->
<!--                    <groupId>org.springframework.boot</groupId>-->
<!--                    <artifactId>spring-boot-starter-tomcat</artifactId>-->
<!--                </exclusion>-->
<!--            </exclusions>--></dependency><!-- 数据库 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis.plus.version}</version></dependency><!-- AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- 拼音 --><dependency><groupId>com.github.promeg</groupId><artifactId>tinypinyin</artifactId><version>${tinypinyin.version}</version></dependency><!-- 后端校验 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- 接口文档 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${swagger.version}</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>${swagger.version}</version></dependency><dependency><groupId>io.swagger</groupId><artifactId>swagger-models</artifactId><version>${swagger.models.version}</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>swagger-bootstrap-ui</artifactId><version>${swagger.triui.version}</version></dependency><!-- 对象转换 --><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${mapStruct.version}</version><scope>provided</scope></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${mapStruct.version}</version><scope>provided</scope></dependency><!-- 验证码 --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>${captcha.version}</version></dependency><!-- 缓存 --><dependency><groupId>org.ehcache</groupId><artifactId>ehcache</artifactId></dependency><!-- 权限控制 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-web-starter</artifactId><version>${shiro.version}</version></dependency><!-- 文件操作 --><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>${commons.io.version}</version></dependency><!-- debug阶段 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><scope>provided</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>provided</scope></dependency><!-- 单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework</groupId>-->
<!--            <artifactId>spring-test</artifactId>-->
<!--            <scope>test</scope>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>junit</groupId>-->
<!--            <artifactId>junit</artifactId>-->
<!--            <scope>test</scope>-->
<!--        </dependency>--></dependencies><build><finalName>jk</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

各种Object

  • 数据库中对于一张表的数据,由于拥有隐私字段、多余字段、字段过少等原因,不应该直接传递给客户端让客户端直接使用。
  • 并且要知道数据的传输是要经过网络通信的,考虑到数据大小对于用户流量、系统并发量/吞吐量等的影响,我们也不应该给客户传递冗余或者缺失的JSON数据。
  • 还有很多原因,比如我们的一个业务所需要的信息有时候其实并不是仅由一张表就能覆盖的、比如数据库中的字段信息其实并不适合展示给用户看,需要做处理、…
  • 综上,我们肯定要对从数据库中查询出来的表数据进行一些加工处理、业务逻辑处理之后再传递给上一层,直到客户端,而不是简单的一张表对应一个Model对象。

前后端分离

以前这种协作模式的问题:

  • 前端地位比较低,大部分工作都在后台
  • 调试、修改页面比较麻烦,需要前端和后台相互充分配合修改动态模板
  • 浪费用户流量以及增加服务器负担:每次请求服务器都会返回整个HTML页面

前后端分离—实现

  1. 前端页面保存使用静态页面HTML
  2. 静态页面使用JS发送异步请求(AJAX)给后台服务器,后台服务器返回JSON数据
  3. 前端使用JS解析JSON数据,动态生成HTML标签,显示在浏览器给用户展示
  4. 前端一个项目、后端一个项目,各自独立开发
  5. 前端一个服务器、后端一个服务器,分开部署

前后端分离—约定数据格式

  • 项目开发应该撰写开发文档,前端照着开发文档编写页面,后端照着开发文档编写API接口
  • 后端和前端之间应该有一种数据格式、有一种协议,双方事先约定好一种数据格式,来规范、辅助开发,比如让后端返回JSON数据
  • 比如后端约定好给前端返回:
    # 错误的返回结果,并且设置HTTP状态码为400/500...
    {"code": 8001,"msg": "密码错误"
    }# 正确的返回结果
    {"code": 8000,"msg": "添加成功","data": [ ... ]
    }
    

跨域问题

  • https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

  • https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin

@RestController
//@CrossOrigin("*") // 所有的源都可以跨域访问该Controller下的请求
//@CrossOrigin({"http://localhost:63343","http://192.168.152.130:8888"})
//@CrossOrigin("http://localhost:63343")
public class UserController {@GetMapping("/users")@CrossOrigin("http://localhost:63343")public List<User> user() {List<User> list = new ArrayList<>();for (int i = 0; i < 10; i++) {list.add(new User("lp" + i, i));}return list;}}

@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {//        registry.addMapping("/users/*")registry.addMapping("/**")
//                .allowedOrigins("*").allowedOrigins("http://localhost:63343").allowCredentials(true) // 允许客户端发送Cookie.allowedMethods("GET", "POST");}
}

项目中可以这样配置:

@Component
@Data
@ConfigurationProperties("cors")
public class CORSProperties {String pathPattern; // 允许哪些路径下的API被跨域访问String[] origins; // 允许跨域请求的源String[] methods; // 允许跨域请求的方法类型boolean allowCredentials; // 是否允许Cookie
//    详细信息可以参考org.springframework.web.bind.annotation.CrossOrigin
//    String[] allowedHeaders;
//    String[] exposedHeaders;
}
cors:path-pattern: /**methods:- GET- POSTorigins:- http://localhost:63343- http://192.168.152.130:8888allow-credentials: true
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate CORSProperties corsProperties;@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping(corsProperties.getPathPattern()).allowedOrigins(corsProperties.getOrigins()).allowCredentials(corsProperties.isAllowCredentials()).allowedMethods(corsProperties.getMethods());}
}

客户端使用

<button type="button" id="load-btn">加载用户信息</button><script src="./js/jquery.min.js"></script>
<script>$(() => {$('#load-btn').click(() => {$.getJSON('http://localhost:8080/jk/users', (users) => {const $table = $('<table>')$(document.body).append($table)for (const user of users) {const $tr = $('<tr>')$table.append($tr)$tr.append(`<td>${user.name}</td>`)$tr.append(`<td>${user.age}</td>`)}})})})
</script>

Layui

  • 官网:http://layuimini.99php.cn/
  • 文档:http://layuimini.99php.cn/docs/
  • 演示:http://layuimini.99php.cn/onepage/v2/index.html
  • Github:https://github.com/zhongshaofa/layuimini/tree/v2-onepage
  • 下载:https://codeload.github.com/zhongshaofa/layuimini/zip/refs/heads/v2-onepage

注意:Layui中的相对路径都是相对于index.html来说的。

注意:layuimini的表格要求服务器返回的JSON数据格式如下:

{"code": 0,"data": [{"id": 1,"name": "职业","value": "job","intro": "一份工作"},{"id": 2,"name": "性格","value": "character","intro": "人的性格"}],"count": 87
}

MySQL建议

最大IP地址:255.255.255.255CREATE TABLE user(age TINYINT UNSIGNED,ip VARCHAR(15) #需要15个字节
)CREATE TABLE user(age TINYINT UNSIGNED,ip INT UNSIGNED #只需要4个字节
)INSERT INTO user VALUES(10, INET_ATON('255.255.255.255'))SELECT INET_NTOA(ip) FROM user

注意:

  • 索引只能给非空列进行优化

  • 不使用外键有可能导致数据的一致性出现问题,因此需要自己在Java代码的业务层做好业务逻辑控制

  • 数据的一致性远远没有数据库的性能重要

  • 高并发&分布式下的系统为了性能更优以及更好维护,不能使用外键。一般而言只要自己在应用层面做好数据库之间关系的维护,那么不使用外键是完全没有问题的。

数据字典

使用PowerDesigner:

/*==============================================================*/
/* DBMS name:      MySQL 5.0                                    */
/* Created on:     2022-05-05 23:16:43                          */
/*==============================================================*/drop table if exists dict_item;drop table if exists dict_type;/*==============================================================*/
/* Table: dict_item                                             */
/*==============================================================*/
create table dict_item
(id                   bigint not null,name                 varchar(20) not null,value                varchar(20) not null,no                   int not null default 0 comment '用来排序,数字越小,优先级越高,越先展示',type_id              bigint comment '该条目所属的数据字典类型',status               int not null default 1 comment '是否启用该条目,0:不启用,1:启用',primary key (id),unique key AK_UK_1 (name, type_id),unique key AK_UK_2 (value, type_id)
);alter table dict_item comment '数据字典每一项具体的内容';/*==============================================================*/
/* Table: dict_type                                             */
/*==============================================================*/
create table dict_type
(id                   bigint not null auto_increment,name                 varchar(20) not null comment '名称是展示在客户端的,是有可能会发生改变的',value                varchar(20) not null comment '值不会发生改变,编写SQL操作数据时,一般使用value而不是name',intro                varchar(100) comment '防止程序员忘记该数据字典类型的作用、功能(根据项目需求可有可无)',primary key (id),unique key AK_UK_1 (name),unique key AK_UK_2 (value)
);alter table dict_type comment '数据字典类型';alter table dict_item add constraint FK_Reference_1 foreign key (type_id)references dict_type (id) on delete restrict on update restrict;

使用IDEA自带的数据库工具+不使用外键+数据库使用最优字段类型:

create table jk.dict_type
(id    smallint unsigned auto_increment comment '主键'primary key,name  varchar(20)  default '' not null comment '名称',value varchar(20)  default '' not null comment '值',intro varchar(100) default '' not null comment '简介',constraint dict_type_name_uindexunique (name),constraint dict_type_value_uindexunique (value)
)comment '数据字典类型';create table jk.dict_item
(id      smallint unsigned auto_increment comment '主键'primary key,name    varchar(20)       default '' not null comment '名称',value   varchar(20)       default '' not null comment '值',type_id smallint unsigned            not null comment '类型id',sn      smallint unsigned default 0  not null comment '排序序号:默认为0,值越大,越优先排列展示',# 其实个人认为enabled这个字段的类型可以设置为bool或者booleanenabled tinyint unsigned  default 1  not null comment '是否启用:0,禁用;1,启用;默认为1',constraint dict_item_name_type_id_uindexunique (name, type_id),constraint dict_item_value_type_id_uindexunique (value, type_id)
)comment '数据字典条目';

客户端:

服务端:

POJO—Query:

@Data
public class PageQuery {private static final int MIN_SIZE = 1;private static final int DEFAULT_SIZE = 10;private long size; // 一页展示多少条数据private long page; // 第几页/*** 查询出来的数据集* 由于将来查出来的类型不确定(VO、BO、PO),因此泛型使用类型通配符*/private List<?> data;private long total; // 总条数private long pages; // 总页数public long getSize() {return size < MIN_SIZE ? DEFAULT_SIZE : size;}public long getPage() {return page < MIN_SIZE ? MIN_SIZE : page;}
}
@EqualsAndHashCode(callSuper = true)
@Data
public class KeywordQuery extends PageQuery {private String keyword;
}
@EqualsAndHashCode(callSuper = true)
@Data
public class DictTypeQuery extends KeywordQuery {}

ServiceImpl:

@Service
@Transactional
public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {@Autowiredprivate DictTypeMapper mapper;@Override@Transactional(readOnly = true)public void list(DictTypeQuery query) {final String keyword = query.getKeyword();LambdaQueryWrapper<DictType> wrapper = new LambdaQueryWrapper<>();// 按照关键字查询if (!StringUtils.isEmpty(keyword)) {wrapper.like(DictType::getName, keyword).or().like(DictType::getIntro, keyword).or().like(DictType::getValue, keyword);}// 按照id降序排序wrapper.orderByDesc(DictType::getId);// 分页查询Page<DictType> page = new Page<>(query.getNo(), query.getSize());mapper.selectPage(page, wrapper);// 更新query对象query.setData(page.getRecords());query.setPages(page.getPages());query.setTotal(page.getTotal());// 如果客户端的查询条件有问题,MyBatisPlus会自动识别并修正,因此可以修改一下query中的查询条件query.setSize(getSize());query.setPage(getCurrent());}
}

Controller:

@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {@Autowiredprivate DictTypeService service;@GetMappingpublic Map<String, Object> list(DictTypeQuery query) {service.list(query);final Map<String, Object> map = new HashMap<>();map.put("msg", "");map.put("data", query.getData());map.put("count", query.getTotal());map.put("code", 0);return map;}@PostMapping("/remove")public Map<String, Object> remove(String id) {// id = "10"// id = "1, 20, 23"final String[] ids = id.split(",");final Map<String, Object> map = new HashMap<>();if (service.removeByIds(Arrays.asList(ids))) {map.put("msg", "删除成功");map.put("code", 0);} else {map.put("msg", "删除失败");map.put("code", 1);}return map;}@PostMapping("/save")public Map<String, Object> save(DictType dictType) {final Map<String, Object> map = new HashMap<>();if (service.saveOrUpdate(dictType)) {map.put("msg", "保存成功");map.put("code", 0);} else {map.put("msg", "保存失败");map.put("code", 1);}return map;}
}

封装MyBatis-Plus方便查询

以查询DictType(数据字典类型)为例

enhance—MPPage、MPQueryWrapper:

public class MPPage<T> extends Page<T> {private final PageQuery query;public MPPage(PageQuery query) {super(query.getPage(), query.getSize());this.query = query;}public void updateQuery() {query.setData(getRecords());query.setPages(getPages());query.setTotal(getTotal());// 如果客户端的查询条件有问题,MyBatis会自动识别并修正,因此可以修改一下query中的查询数据query.setSize(getSize());query.setPage(getCurrent());}
}
public class MPQueryWrapper<T> extends LambdaQueryWrapper<T> {@SafeVarargspublic final MPQueryWrapper<T> like(Object val, SFunction<T, ?>... funcs) {if (val == null || funcs == null || funcs.length == 0) return this;final String str = val.toString();if (str.length() == 0) return this;return (MPQueryWrapper<T>) nested((wrapper) -> {for (SFunction<T, ?> func : funcs) {wrapper.like(func, str).or();}});}
}

动态代理更新Query对象:

@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {}
@Aspect
@Component
public class PageMapperInterceptor {@Around("execution(public com.baomidou.mybatisplus.core.metadata.IPage com.baomidou.mybatisplus.core.mapper.BaseMapper.selectPage(com.baomidou.mybatisplus.core.metadata.IPage, com.baomidou.mybatisplus.core.conditions.Wrapper))")public Object updateQuery(ProceedingJoinPoint point) throws Throwable {Object result = point.proceed();final Object[] args = point.getArgs();if (args != null && args.length > 0) {Object arg = args[0];if (arg instanceof MPPage) {((MPPage<?>) arg).updateQuery();}}return result;}
}

使用—ServiceImpl:

@Transactional
@Service
public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {@Override@Transactional(readOnly = true)public void list(DictTypeQuery query) {MPQueryWrapper<DictType> wrapper = new MPQueryWrapper<>();wrapper.like(query.getKeyword(), DictType::getName, DictType::getValue, DictType::getIntro);wrapper.orderByDesc(DictType::getId);baseMapper.selectPage(new MPPage<>(query), wrapper);}
}

Controller:

@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {@Autowiredprivate DictTypeService service;@GetMappingpublic Map<String, Object> list(DictTypeQuery query) {service.list(query);final Map<String, Object> map = new HashMap<>();map.put("msg", "");map.put("data", query.getData());map.put("count", query.getTotal());map.put("code", 0);return map;}
}

封装给客户端的返回值

public class R extends HashMap<String, Object> {public static final int CODE_SUCCESS = 0;private static final String K_CODE = "code";private static final String K_MSG = "msg";private static final String K_DATA = "data";public R setCode(int code) {return add(K_CODE, code);}public R setMsg(String msg) {return add(K_MSG, msg);}public R setData(Object data) {return add(K_DATA, data);}public R add(String key, Object data) {put(key, data);return this;}
}
public final class Rs {private Rs() {}// 事先和前端约定好:成功code:0,失败code:1public static final int CODE_SUCCESS = 0;public static final int CODE_ERROR_DEFAULT = 1;private static R success() {return new R().setCode(CODE_SUCCESS);}public static R success(PageQuery query) {return success().setData(query.getData());}public static R success(String msg) {return success().setMsg(msg);}public static R success(PageQuery query, String msg) {return success().setData(query.getData()).setMsg(msg);}public static R error() {return new R().setCode(CODE_ERROR_DEFAULT);}public static R error(String msg) {return error().setMsg(msg);}public static R error(int code, String msg) {return new R().setCode(code).setMsg(msg);}public static R r(boolean success) {return new R().setCode(success ? CODE_SUCCESS : CODE_ERROR_DEFAULT);}public static R r(boolean success, String msg) {return r(success).setMsg(msg);}public static R r(boolean success, Object data) {return r(success).setData(data);}
}

Controller使用:

@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {@Autowiredprivate DictTypeService service;@GetMappingpublic R list(DictTypeQuery query) {service.list(query);return Rs.success(query).add("count", query.getTotal());}@PostMapping("/remove")public R remove(String id) {// id = "10"// id = "1, 20, 23"final String[] ids = id.split(",");//        if (service.removeByIds(Arrays.asList(ids))) {//            return Rs.success("删除成功");
//        } else {//            return Rs.error("删除失败");
//        }final boolean success = service.removeByIds(Arrays.asList(ids));final String msg = success ? "删除成功" : "删除失败";return Rs.r(success, msg);}@PostMapping("/save")public R save(DictType dictType) {if (!service.saveOrUpdate(dictType)) {throw new RuntimeException("保存失败");}return Rs.success("保存成功");}
}

统一异常处理+HTTP响应状态码

  • 如果服务器端操作失败的话,比如删除失败、保存失败,那么给客户端返回的StatusCode就不应该是200,应该是400/500,原因如下:

  • 客户端(前端)极大可能是根据HTTP请求的响应状态码来判断某个请求是否成功的,而不是通过服务器返回的JSON数据的某个属性值来判断

  • 比如AJAX的回调方法默认就是通过HTTP的响应状态码来判断是否请求成功的。

  • 因此如果服务器处理数据失败,应该修改响应状态码200(OK)为其它StatusCode,比如400、500。

    public interface JSONable {default String jsonString() throws Exception {return JSONs.getMAPPER().writeValueAsString(this);}
    }
    
    public class R extends HashMap<String, Object> implements JSONable {// ...
    }
    
    @ControllerAdvice
    public class ExceptionInterceptor {// 默认处理所有的异常@ExceptionHandler(Throwable.class)public void exceptionHandlerOther(Throwable throwable, HttpServletResponse response) throws Exception {response.setCharacterEncoding("UTF-8");response.setStatus(400);
    //        response.getWriter().write(Rs.error(getRealCause(throwable).getMessage()).jsonString());response.getWriter().write(Rs.error(throwable.getMessage()).jsonString());}private Throwable getRealCause(Throwable throwable) {Throwable cause = throwable.getCause();while (cause != null) {throwable = cause;cause = cause.getCause();}return throwable;}
    }
    
    @RestController
    @RequestMapping("/dictTypes")
    public class DictTypeController {@Autowiredprivate DictTypeService service;@PostMapping("/remove")public R remove(String id) {final String[] ids = id.split(",");if (!service.removeByIds(Arrays.asList(ids))) {throw new RuntimeException("删除失败");}return Rs.success("删除成功");}
    }
    
  • 还可以继续封装:

    public enum CodeMsg {BAD_REQUEST(400, "请求出错"),UNAUTHORIZED(401, "未授权"),FORBIDDEN(403, "禁止访问"),NOT_FOUND(404, "资源不存在"),INTERNAL_SERVER_ERROR(500, "服务器内部错误"),OPERATE_OK(R.CODE_SUCCESS, "操作成功"),SAVE_OK(R.CODE_SUCCESS, "保存成功"),REMOVE_OK(R.CODE_SUCCESS, "删除成功"),OPERATE_ERROR(40001, "操作失败"),SAVE_ERROR(40002, "保存失败"),REMOVE_ERROR(40003, "删除失败"),UPLOAD_IMG_ERROR(40004, "图片上传失败"),WRONG_USERNAME(50001, "用户名不存在"),WRONG_PASSWORD(50002, "密码错误"),USER_LOCKED(50003, "用户被锁定,无法正常登录"),WRONG_CAPTCHA(50004, "验证码错误"),NO_TOKEN(60001, "没有Token,请登录"),TOKEN_EXPIRED(60002, "Token过期,请重新登录"),NO_PERMISSION(60003, "没有相关的操作权限");private final int code;private final String msg;CodeMsg(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}
    }
    
    @EqualsAndHashCode(callSuper = true)
    @Data
    public class CommonException extends RuntimeException {private int code;public CommonException() {this(CodeMsg.BAD_REQUEST.getCode(), null);}public CommonException(String msg) {this(msg, null);}public CommonException(int code, String msg) {this(code, msg, null);}public CommonException(String msg, Throwable cause) {this(CodeMsg.BAD_REQUEST.getCode(), msg, cause);}public CommonException(int code, String msg, Throwable cause) {super(msg, cause);this.code = code;}public CommonException(CodeMsg codeMsg) {this(codeMsg, null);}public CommonException(CodeMsg codeMsg, Throwable cause) {this(codeMsg.getCode(), codeMsg.getMsg(), cause);}public int getCode() {return code;}
    }
    
    public final class Rs {private Rs() {}public static final String K_COUNT = "count";private static final int CODE_SUCCESS = 0;private static final int CODE_ERROR_DEFAULT = CodeMsg.BAD_REQUEST.getCode();private static R success() {return new R().setCode(CODE_SUCCESS);}public static R success(PageQuery query) {return success().setData(query.getData());}public static R success(String msg) {return success().setMsg(msg);}public static R success(Object data) {return success().setData(data);}public static R success(PageQuery query, String msg) {return success().setData(query.getData()).setMsg(msg);}public static R success(CodeMsg codeMsg) {return success().setMsg(codeMsg.getMsg());}public static R error() {return error(CODE_ERROR_DEFAULT);}public static R error(int code) {return new R().setCode(code);}public static R error(String msg) {return error().setMsg(msg);}public static R error(int code, String msg) {return error(code).setMsg(msg);}public static R error(Throwable e) {//        R r = error(e.getMessage()); // 开发阶段R r = error(); // 项目上线 项目上线了就不要把其它异常信息给用户看了if (e instanceof CommonException) {r.setCode(((CommonException) e).getCode());}return r;}public static R r(boolean success) {return new R().setCode(success ? CODE_SUCCESS : CODE_ERROR_DEFAULT);}public static R r(boolean success, String msg) {return r(success).setMsg(msg);}public static R r(boolean success, Object data) {return r(success).setData(data);}public static R exception(String msg) {throw new CommonException(msg);}public static R exception(CodeMsg codeMsg) {throw new CommonException(codeMsg);}
    }
    
    @ControllerAdvice
    public class ExceptionInterceptor {@ExceptionHandler(Throwable.class)public void handle(Throwable throwable,HttpServletResponse response) throws Exception {//        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE/*"application/json; charset=UTF-8"*/);response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());response.setStatus(400);
    //        response.getWriter().write(Rs.error(getRealCause(throwable)).json());response.getWriter().write(Rs.error(throwable).json());}private Throwable getRealCause(Throwable throwable) {Throwable cause = throwable.getCause();while (cause != null) {throwable = cause;cause = cause.getCause();}return throwable;}
    }
    
  • 其实异常处理器还有更简便的写法:

    @RestControllerAdvice
    @Slf4j
    public class CommonExceptionHandler {@ExceptionHandler(Throwable.class)@ResponseStatus(code = HttpStatus.BAD_REQUEST)public JSONResult handle(Throwable throwable) {//        System.out.println(throwable);log.error("error", throwable);return JSONResults.exception(throwable);}
    }
    
  • Controller使用:

    @RestController
    @RequestMapping("/dictTypes")
    public class DictTypeController {@Autowiredprivate DictTypeService service;@GetMappingpublic R list(DictTypeQuery query) {service.list(query);return Rs.success(query).add(Rs.K_COUNT, query.getTotal());}@PostMapping("/remove")public R remove(String id) {// id = "10"// id = "1, 20, 23"final String[] ids = id.split(",");if (!service.removeByIds(Arrays.asList(ids))) {throw new CommonException(CodeMsg.REMOVE_ERROR);}return Rs.success(CodeMsg.REMOVE_OK);}@PostMapping("/save")public R save(DictType dictType) {if (!service.saveOrUpdate(dictType)) {throw new CommonException(CodeMsg.SAVE_ERROR);}return Rs.success(CodeMsg.SAVE_OK.getMsg());}
    }
    
  • 还可以将Controller中的公共代码抽取出来:

    public abstract class BaseController<T> {protected abstract IService<T> service();@GetMapping("/list")public R list() {return Rs.success(service().list());}@PostMapping("/remove")public R remove(String id) {final String[] ids = id.split(",");if (!service().removeByIds(Arrays.asList(ids))) {Rs.exception(CodeMsg.REMOVE_ERROR);}// return Rs.success(CodeMsg.REMOVE_OK.getMsg());return Rs.success(CodeMsg.REMOVE_OK);}@PostMapping("/save")public R save(T entity) {if (!service().saveOrUpdate(entity)) {// throw new CommonException(CodeMsg.SAVE_ERROR);Rs.exception(CodeMsg.SAVE_ERROR);}return Rs.success(CodeMsg.SAVE_OK);}
    }
    
    @RestController
    @RequestMapping("/dictTypes")
    public class DictTypeController extends BaseController<DictType> {@Autowiredprivate DictTypeService service;@GetMappingpublic R list(DictTypeQuery query) {service.list(query);return Rs.success(query).add(Rs.K_COUNT, query.getTotal());}@Overrideprotected IService<DictType> service() {return service;}
    }
    

统一异常处理—配合Shiro

@RestControllerAdvice只能拦截到Controller抛出的异常

public class ErrorFilter implements Filter {public static final String ERROR_URI = "/handleError";@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {try {chain.doFilter(request, response);} catch (Exception e) {request.setAttribute(ERROR_URI, e);request.getRequestDispatcher(ERROR_URI).forward(request, response);}}
}
@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {@Beanpublic FilterRegistrationBean<Filter> filterRegistrationBean() {FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();// 设置Filterbean.setFilter(new ErrorFilter());bean.addUrlPatterns("/*");// 最高权限bean.setOrder(Ordered.HIGHEST_PRECEDENCE);return bean;}
}
@RestController
public class ErrorController {@RequestMapping(ErrorFilter.ERROR_URI)public void handle(HttpServletRequest request) throws Exception {// 抛出异常throw (Exception) request.getAttribute(ErrorFilter.ERROR_URI);}
}
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {@ExceptionHandler(Throwable.class)@ResponseStatus(code = HttpStatus.BAD_REQUEST)public JSONResult handle(Throwable t) {log.error("handle", t);// 一些可以直接处理的异常if (t instanceof CommonException) {return handle((CommonException) t);} else if (t instanceof BindException) {return handle((BindException) t);} else if (t instanceof ConstraintViolationException) {return handle((ConstraintViolationException) t);} else if (t instanceof AuthorizationException) {return JSONResults.error(CodeMsg.NO_PERMISSION);}// 处理cause异常(导致产生t的异常)Throwable cause = t.getCause();if (cause != null) {return handle(cause);}// 其他异常(没有cause的异常)return JSONResults.error();}private JSONResult handle(CommonException ce) {return JSONResults.error(ce.getCode(), ce.getMessage());}private JSONResult handle(BindException be) {List<ObjectError> errors = be.getBindingResult().getAllErrors();// 函数式编程的方式:streamList<String> defaultMsgs = Streams.map(errors, ObjectError::getDefaultMessage);String msg = StringUtils.collectionToDelimitedString(defaultMsgs, ", ");return JSONResults.error(msg);}private JSONResult handle(ConstraintViolationException cve) {List<String> msgs = Streams.map(cve.getConstraintViolations(), ConstraintViolation::getMessage);String msg = StringUtils.collectionToDelimitedString(msgs, ", ");return JSONResults.error(msg);}
}
@Configuration
public class ShiroConfig {/*** ShiroFilterFactoryBean用来告诉Shiro如何进行拦截* 1.拦截哪些URL* 2.每个URL需要经过哪些filter*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(Realm realm/*, JkProperties properties*/) {ShiroFilterFactoryBean filterBean = new ShiroFilterFactoryBean();// 安全管理器filterBean.setSecurityManager(new DefaultWebSecurityManager(realm));// 添加一些自定义FilterMap<String, Filter> filters = new HashMap<>();filters.put("token", new TokenFilter());filterBean.setFilters(filters);// 设置URL如何拦截// Filter的顺序很重要,因此需要使用LinkedHashMapMap<String, String> urlMap = new LinkedHashMap<>();// ...// 放行全局Filter的异常处理urlMap.put(ErrorFilter.ERROR_URI, "anon");// 其他urlMap.put("/**", "token");filterBean.setFilterChainDefinitionMap(urlMap);return filterBean;}
}

数据的一致性

外键有优点,同样,也有它的缺点

  • 如果在项目中使用了外键,那么表与表之间的数据一致性其实是不用我们操心的,因为有外键自动帮我们约束。
  • 因此对于那些小型的、对于数据一致性要求很高的项目,需要使用外键。
  • 但是大型的、分布式的互联网项目出于对数据库的性能、备份、迁移、维护等原因的考虑,一般而言在设计数据库时是不使用外键的,那么这时表与表之间的联系,也就是数据一致性问题怎么解决呢?
  • 答案就是需要我们自己在应用层做好相应的处理,但做好这个处理并不简单。

假设现在要对某张表的进行数据一致性的处理,有许多非常麻烦的点

  • 虽然数据库没有使用外键,但对业务来讲,表与表之间应该是有联系的
  • 那么对这张表进行删除、更新、添加等操作都需要考虑数据一致性,具体到代码,就是remove、save、update这种方法有很多,需要都考虑到
  • 有可能会有很多表在数据上都关联这张表,因此我们需要清楚每一张表与每一张表之间的关联关系,知道了表与表之间的关联关系后,才能去逐个处理

解决方案:自己写一个保证数据一致性的小框架,这个小框架的特点:

  • 注解驱动
  • AOP
  • 反射

MJ老师编写框架经验:

  • 如果你自己想写一个比较好用的框架
  • 首先应该从应用的角度出发,先从使用者(自己、其他开发者)的角度出发
  • 考虑别人应该怎么用这个框架、这个框架能够怎样简化开发、这个框架怎么样能够使开发变得更爽、更高效、更敏捷
  • 然后再考虑减少BUG
  • 然后再考虑安全问题
  • 然后再考虑性能问题
  • 然后再考虑解耦、抽取、可扩展…

拼音库—tinypinyin的使用

 <!-- 拼音 --><dependency><groupId>com.github.promeg</groupId><artifactId>tinypinyin</artifactId><version>2.0.3</version></dependency>
    // IService.saveOrUpdate方法里面会调用updateById或者save方法,因此需要重写这两个方法对拼音进行处理@Overridepublic boolean updateById(PlateRegion entity) {processPinyin(entity);return super.updateById(entity);}@Overridepublic boolean save(PlateRegion entity) {processPinyin(entity);return super.save(entity);}private void processPinyin(PlateRegion entity) {final String name = entity.getName();if (StringUtils.isEmpty(name)) return;entity.setPinyin(Pinyin.toPinyin(name, "_"));}

MapStruct

作用:对象转换

  • po ——> vo
  • vo ——> po
 <dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${map.struct.version}</version><scope>provided</scope></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${map.struct.version}</version><scope>provided</scope></dependency>
@Mapper
public interface MapStruct {MapStruct INSTANCE = Mappers.getMapper(MapStruct.class);DictItem vo2po(ReqSaveDictItem reqSaveVo);DictType vo2po(ReqSaveDictType reqSaveVo);ExamPlace vo2po(ReqSaveExamPlace reqSaveVo);ExamPlaceCourse vo2po(ReqSaveExamPlaceCourse reqSaveVo);PlateRegion vo2po(ReqSavePlateRegion reqSaveVo);RespDictItem po2vo(DictItem po);RespDictType po2vo(DictType po);RespExamPlace po2vo(ExamPlace po);RespExamPlaceCourse po2vo(ExamPlaceCourse po);RespPlateRegion po2vo(PlateRegion po);
}

基本使用:

 final DictItem dictItem = MapStruct.INSTANCE.vo2po(new ReqSaveDictItem());final RespDictItem respDictItem = MapStruct.INSTANCE.po2vo(new DictItem());

项目中使用:

public abstract class BaseController<T, ReqSave> {protected abstract Function<ReqSave, T> function();// ...@PostMapping("/save")public JSONResult save(@Valid ReqSave entity) {service.saveOrUpdate(function().apply(entity));}
}
public class DictItemController extends BaseController<DictItem, ReqSaveDictItem> {// ...@Overrideprotected Function<ReqSaveDictItem, DictItem> function() {return MapStruct.INSTANCE::vo2po;}
}
public class PlateRegionServiceImpl extends ServiceImpl<PlateRegionMapper, PlateRegion> implements PlateRegionService {public JSONDataResult<List<RespPlateRegion>> listProvinces() {// ...final List<RespPlateRegion> data = baseMapper.selectList(wrapper).stream().map(MapStruct.INSTANCE::po2vo).collect(Collectors.toList());return JSONResults.success(data);}
}

自定义转换规则

public class MapStructFormatter {@Qualifier@Target(ElementType.METHOD)@Retention(RetentionPolicy.CLASS)public @interface Date2Millis {}@Date2Millispublic static Long date2millis(Date date) {if (date == null) return null;return date.getTime();}/*@Qualifier@Target(ElementType.METHOD)@Retention(RetentionPolicy.CLASS)public @interface Mills2Date {}@Mills2Datepublic static Date millis2date(Long mills) {if (mills == null) return null;return new Date(mills);}*/
}
@Data
public class SysUser {// ...//最后一次登录的时间private Date loginTime;
}
@Data
@ApiModel("系统用户")
public class RespSysUser {// ...@ApiModelProperty("最后一次登录的时间")// 前后端分离一般返回UNIX时间戳// UNIX时间戳:从 1970-1-1 0:0:0 开始到现在走过的毫秒数private Long loginTime;
}
/*** ReqVo -> Po* Po -> Vo*/
@Mapper(uses = {MapStructFormatter.class
})
public interface MapStructs {// ...@Mapping(source = "loginTime",target = "loginTime",qualifiedBy = MapStructFormatter.Date2Millis.class)RespSysUser po2vo(SysUser po);// ...
}

登录—简单登录

 <!-- 验证码 --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency>

@RestController
@RequestMapping("/sysUsers")
@Api(tags = "系统用户", description = "SysUser")
public class SysUserController extends BaseController<SysUser, ReqSaveSysUser> {@Autowiredprivate SysUserService service;@GetMapping("/captcha")@ApiOperation("生成验证码")public void captcha(HttpServletRequest request,HttpServletResponse response) throws Exception {CaptchaUtil.out(request, response);}@PostMapping("/login")@ApiOperation("登录")public JSONDataResult<RespLogin> login(ReqLogin reqVo, HttpServletRequest request) {if (CaptchaUtil.ver(reqVo.getCaptcha(), request)) {return JSONResults.success(service.login(reqVo));}JSONResults.exception(CodeMsg.WRONG_CAPTCHA);return null;}
}
 @Overridepublic RespLogin login(ReqLogin reqVo) {// 根据用户名查询用户MPLambdaQueryWrapper<SysUser> wrapper = new MPLambdaQueryWrapper<>();wrapper.eq(SysUser::getUsername, reqVo.getUsername());SysUser po = baseMapper.selectOne(wrapper);// 用户名不存在if (po == null) {return JsonVos.raise(CodeMsg.WRONG_USERNAME);}// 密码不正确if (!po.getPassword().equals(reqVo.getPassword())) {return JsonVos.raise(CodeMsg.WRONG_PASSWORD);}// 账号锁定if (po.getStatus() == Constants.SysUserStatus.LOCKED) {return JsonVos.raise(CodeMsg.USER_LOCKED);}// 登录成功// 更新登录时间po.setLoginTime(new Date());baseMapper.updateById(po);// 返回给客户端的具体数据RespLogin vo = MapStruct.INSTANCE.po2loginVo(po);return vo;}

前端Ajax登录:

Ajaxs.loadPost({uri: 'sysUsers/login',data: data.field,success: (response) => {location.href = '../index.html'},xhrFields: { // 需要跨域带上cookiewithCredentials: true}
}

登录—Token

后端

  • 登录

      @Data@ApiModel("登录成功的结果")public class RespLogin {// ...@ApiModelProperty("登录令牌")private String token;}@PostMapping("/login")@ApiOperation("登录")public JSONDataResult<RespLogin> login(ReqLogin reqVo, HttpServletRequest request) {if (CaptchaUtil.ver(reqVo.getCaptcha(), request)) {return JSONResults.success(service.login(reqVo));}JSONResults.exception(CodeMsg.WRONG_CAPTCHA);return null;}@Overridepublic RespLogin login(ReqLogin reqVo) {// 根据用户名查询用户MPLambdaQueryWrapper<SysUser> wrapper = new MPLambdaQueryWrapper<>();wrapper.eq(SysUser::getUsername, reqVo.getUsername());SysUser po = baseMapper.selectOne(wrapper);// 用户名不存在if (po == null) {return JsonVos.raise(CodeMsg.WRONG_USERNAME);}// 密码不正确if (!po.getPassword().equals(reqVo.getPassword())) {return JsonVos.raise(CodeMsg.WRONG_PASSWORD);}// 账号锁定if (po.getStatus() == Constants.SysUserStatus.LOCKED) {return JsonVos.raise(CodeMsg.USER_LOCKED);}/**** 登录成功 ****/// 更新登录时间po.setLoginTime(new Date());baseMapper.updateById(po);// 生成Token,发送Token给用户String token = UUID.randomUUID().toString();// 存储token到缓存中Caches.putToken(token, po);// 返回给客户端的具体数据RespLogin vo = MapStruct.INSTANCE.po2loginVo(po);vo.setToken(token);return vo;}
    
  • 退出登录

        @PostMapping("/logout")@ApiOperation("退出登录")public JSONResult logout(@RequestHeader("Token") String token) {Caches.removeToken(token);return JSONResults.success(CodeMsg.LOGOUT_OK);}
    

前端

  • 登录:

    class DataKey {static USER = 'user'static TOKEN = 'token'static TOKEN_HEADER = 'Token'
    }
    
       Ajaxs.loadPost({uri: 'sysUsers/login',data: data.field,success: (response) => {Datas.save(DataKey.USER, response.data)location.href = '../index.html'},xhrFields: { // 需要跨域带上cookiewithCredentials: true}})
    
  • 需要确保登录后的每次请求都带上Token信息

        static get() { // Datas.getlet ret = layui.data(this.TABLE)for (let i = 0; i < arguments.length; i++) {if (!ret) return nullret = ret[arguments[i]]}return ret}static _addTokenHeader(cfg) {// 取出tokenconst token = Datas.get(DataKey.USER, DataKey.TOKEN)if (token) {if (!cfg.headers) {cfg.headers = {}}// 将token放到请求头cfg.headers[DataKey.TOKEN_HEADER] = token}}
    
       // 自己封装的Ajax请求,每一次请求都需要带上Token信息static ajax(cfg) {cfg.url = Commons.url(cfg.uri)Commons._addTokenHeader(cfg)$.ajax(cfg)}
    
       // Layui发送请求时也需要带上Token信息_init() {const cfg = this._commonCfg()cfg.url = Commons.url(this._cfg.uri)$.extend(cfg, this._cfg)cfg.elem = cfg.selectorCommons._addTokenHeader(cfg)this._innerTable = this._layuiTable().render(cfg)this._cfg = cfg}
    
  • 登出:

       $('.login-out').click(() => {// 发送请求给服务器:退出登录Ajaxs.loadPost({uri: 'sysUsers/logout',success: () => {// 清除客户端缓存Datas.remove(DataKey.USER)// 提示Layers.msgSuccess('退出登录成功', () => {location.href = 'page/login.html'})}})})
    

权限管理—RBAC

可以登录后台管理系统的员工/系统管理员:比如:sys_user(表名以sys_开头)

使用产品的用户/客户(APP、小程序、网页):比如:user

表结构设计

可以登录后台管理系统的员工/系统管理员:比如:sys_user(表名以sys_开头)

使用产品的用户/客户(APP、小程序、网页):比如:user

create table if not exists jk.sys_resource
(id tinyint unsigned auto_increment comment '主键'primary key,name varchar(15) default '' not null comment '名称',uri varchar(100) default '' not null comment '链接地址',permission varchar(100) default '' not null comment '权限标识',type tinyint unsigned default 0 not null comment '资源类型(0是目录,1是菜单,2是按钮)PS:按钮就是增删改查之类的能点击的',icon varchar(100) default '' not null comment '图标',sn tinyint unsigned default 0 not null comment '序号',parent_id tinyint unsigned default 0 not null comment '父资源id',constraint sys_resource_parent_id_name_uindexunique (parent_id, name)
)
comment '资源';create table if not exists jk.sys_role
(id tinyint unsigned auto_increment comment '主键'primary key,name varchar(15) default '' not null comment '角色名称',constraint sys_role_name_uindexunique (name)
)
comment '角色';create table if not exists jk.sys_role_resource
(role_id tinyint unsigned default 0 not null comment '角色id',resource_id tinyint unsigned default 0 not null comment '资源id',primary key (resource_id, role_id)
)
comment '角色-资源';create table if not exists jk.sys_user
(id smallint unsigned auto_increment comment '主键'primary key,nickname varchar(15) default '' not null comment '昵称',username varchar(15) default '' not null comment '登录用的用户名',password char(32) default '' not null comment '登录用的密码,密码经过MD5加密之后就是32位的字符串',create_time datetime default CURRENT_TIMESTAMP not null comment '创建的时间',login_time datetime null comment '最后一次登录的时间',status tinyint unsigned default 0 not null comment '账号的状态,0是正常,1是锁定',constraint sys_user_username_uindexunique (username)
)
comment '用户(可以登录后台系统的)';create table if not exists jk.sys_user_role
(role_id tinyint unsigned default 0 not null comment '角色id',user_id smallint unsigned default 0 not null comment '用户id',primary key (user_id, role_id)
)
comment '用户-角色';

逻辑删除

  • 物理删除:真正从数据库中删除了,永久消失。

  • 逻辑删除(假删除、软删除):数据还在数据库中,只是对用户来说,数据被删掉了。

  • 逻辑删除的实现:在需要实现逻辑删除的表中增加一个字段来标识数据是否被删除。

逻辑删除—MyBatisPlus

# db: test
create table user
(id int unsigned auto_incrementprimary key,name varchar(15) default '' not null,deleted tinyint unsigned default 0 not null comment '1是被删除,0是未删除',constraint user_name_uindexunique (name)
);

企业级文件上传

  • 方案一:文件数据和表单数据一起提交(之前Java②学过的那种)

  • 方案二:专门弄一个文件服务器,用来操作文件(文件上传、下载、删除等)。文件数据先单独提交,从文件服务器返回一个uri,拿着这个uri和表单数据一起提交【前端需要进行用户的行为控制,比较复杂一点】

@RequestBody修饰的请求参数

  1. 前端/客户端将Content-Type改为:application/json

  2. 请求体传递符合要求的JSON字符串【JSON优势:灵活、前端处理方便、第三方库也多】

        @PostMapping("/save1")@ApiOperation("添加或更新")public JsonVo save1(User user) { // 相当于是加了@RequestParamJsonVo jsonVo = new JsonVo();jsonVo.setMsg(service.saveOrUpdate(user) ? "保存成功" : "保存失败");return jsonVo;}@PostMapping("/save2")@ApiOperation("添加或更新")public JsonVo save2(@RequestBody User user) { // 要求前端客户端传一个User类型的JSON字符串过来,请求体是一个JSONJsonVo jsonVo = new JsonVo();jsonVo.setMsg(service.saveOrUpdate(user) ? "保存成功" : "保存失败");return jsonVo;}
    

单元测试

Spring单元测试

<!-- 单元测试 -->
<!--        <dependency>-->
<!--            <groupId>org.springframework</groupId>-->
<!--            <artifactId>spring-test</artifactId>-->
<!--            <scope>test</scope>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>junit</groupId>-->
<!--            <artifactId>junit</artifactId>-->
<!--            <scope>test</scope>-->
<!--        </dependency>-->

SpringBoot单元测试

 <!-- 单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {@Autowiredprivate RabbitTemplate rabbitTemplate;@Testpublic void testSendSimpleQueue() {String queueName = "simple.queue";Object message = "Hello, SpringAMQP! I am LP!";rabbitTemplate.convertAndSend(queueName, message);}
}

打包部署

打包部署—jar

打包部署—war


注意

配置JackSON将Model转为JSON时,不包含值为null的属性:

  • application.yml:

    spring:jackson:default-property-inclusion: non_null
    
  • Java代码:

    public final class JSONs {private JSONs() {}private static final ObjectMapper MAPPER = new ObjectMapper();static {MAPPER.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);}public static ObjectMapper getMAPPER() {return MAPPER;}
    }
    

  • MySQL配置:

    #url=jdbc:mysql://127.0.0.1:3306/lp_resume
    #url=jdbc:mysql://localhost:3306/lp_resume
    #url=jdbc:mysql:///lp_resume#UTC:世界同一时间
    #url=jdbc:mysql:///lp_resume?serverTimezone=UTC&useSSL=false
    #中国时间:serverTimezone=Asia/Shanghai == serverTimezone=GMT+8
    #url=jdbc:mysql:///lp_resume?serverTimezone=GMT+8&useSSL=false
    url=jdbc:mysql:///lp_resume?serverTimezone=Asia/Shanghai&useSSL=false############使用IDEA连接数据库############
    #使用IDEA连接MySQL数据库时,由于“+”是一个特殊字符,因此需要编码处理为:“%2B”
    #例如:jdbc:mysql:///?serverTimezone=GMT%2B8&useSSL=false
    #或者:jdbc:mysql:///?serverTimezone=Asia/Shanghai&useSSL=false
    
  • HTML的button标签,默认类型是<button type="submit"></button>,因此button如果是其它类型的话,最好显示声明button的type,比如:<button type="button"/>

  • 客户端向服务器发送请求参数时

    • 如果http://localhost:8080/jk/dictTypes/list?page=1&size=20,那么服务器获取到的keyword就是null
    • 如果http://localhost:8080/jk/dictTypes/list?page=1&size=20&keyword=,那么服务器获取到的keyword就是""(空字符串)
  • 数据库中,表名和字段名建议使用``、字符串建议使用’'(单引号)

  • MySQL数据库,行(记录)从0开始,列(字段)从1开始

  • 标准JSON格式:key使用""(双引号):

    [{"age": 10,"name": "lp"},{"age": 20,"name": "ruoyu"}
    ]
    
    {"string": "value","integer": 10,"bool": true,"null": null,"array": [],"obj": {}
    }
    

补充:ChromeJSON插件

https://chrome.google.com/webstore/detail/json-handle/iahnhfdhidomcpggpaimmmahffihkfnj

参考

小码哥-李明杰: Java从0到架构师③进阶互联网架构师.


本文完,感谢您的关注支持!


【Java从零到架构师第③季】【项目实战】驾考管理系统相关推荐

  1. 【Java从零到架构师第③季】【49】会话管理—Token_ehcache

    持续学习&持续更新中- 守破离 [Java从零到架构师第③季][49]会话管理-Token_ehcache 基于Cookie.Session 基于Token ehcache 简单使用 项目使用 ...

  2. 【Java从零到架构师第③季】【48】SpringBoot-Swagger

    持续学习&持续更新中- 守破离 [Java从零到架构师第③季][48]SpringBoot-Swagger 接口文档-Swagger 基本使用 不使用starter 使用starter(Swa ...

  3. 【Java从零到架构师第③季】【26】SpringMVC-反射获取方法参数名_SpringMVC是如何获取方法的参数名的

    持续学习&持续更新中- 守破离 [Java从零到架构师第③季][26]SpringMVC-反射获取方法参数名_SpringMVC是如何获取方法的参数名的 利用反射获取方法的参数名 直接编译 修 ...

  4. 【Java从零到架构师第③季】【24】SpringMVC-概述_入门

    持续学习&持续更新中- 守破离 [Java从零到架构师第③季][24]SpringMVC-概述_入门 Spring.SpringMVC.MyBatis之间的关系 SpringMVC简介 Spr ...

  5. 【Java从零到架构师第③季】【28】SpringMVC-Servlet的URL匹配_path-matching suffix-pattern

    持续学习&持续更新中- 守破离 [Java从零到架构师第③季][28]SpringMVC-Servlet的URL匹配_path-matching suffix-pattern Servlet的 ...

  6. 【Java从零到架构师第二季】【07】JDBC FOR MySQL

    持续学习&持续更新中- 学习态度:守破离 JDBC FOR MySQL 什么是JDBC 如何通过Java操作数据库 JDBC是属于JavaSE的一部分 下载MySQL的JDBC实现 JDBC细 ...

  7. 【Java从零到架构师第二季】【14】AJAX

    持续学习&持续更新中- 学习态度:守破离 AJAX 同步请求和异步请求 未学AJAX之前向服务器提交请求的方式 同步和异步 AJAX 什么是AJAX AJAX的常见使用方式 原生 jQuery ...

  8. 【Java从零到架构师第1季】【并发 Concurrent 03】线程间通信_ReentrantLock_线程池

    持续学习&持续更新中- 守破离 [Java从零到架构师第1季][并发 Concurrent 03]线程间通信_ReentrantLock_线程池 线程间通信 线程间通信-示例 可重入锁Reen ...

  9. 【Java从0到架构师】项目实战 - 驾考(旧)- Freemarker、MyBatis-Plus

    项目实战 - 驾考(旧)- Freemarker.MyBatis-Plus 各种 Object Freemarker 简单使用 常用功能 集成到 SpringBoot MyBatis-Plus 简单使 ...

  10. 个人总结的一个中高级Java开发工程师或架构师需要掌握的一些技能...

    近三年,其实都是在做一个项目,项目是一个大型的多节点部署的项目,做了好几个版本,中间用到了很多技术和框架, 也用了一些管理工具和敏捷实践.我这里不是来说项目的,因为最近看了一些招聘信息,结合项目中用到 ...

最新文章

  1. 和12岁小同志搞创客开发:设计一款亮度可调节灯
  2. 关于python和anaconda的一些基础认识
  3. 【偶尔一道ctf】xctf adword mobile easy-apk
  4. python读文件写文件-python 文件读写操作
  5. Node.js流,这样的打开方式对不对!
  6. with...as...
  7. 入侵检测系统基础知识
  8. 条件随机场CRF简介Introduction to Conditional Random Fields
  9. 五子棋开发案列c语言,C语言案例:控制台版本的五子棋游戏【代码】
  10. 解决win11 WSL下通过systemd无法启动docker的问题:改为dockerd手动启动
  11. mybatis多对一处理两种处理方式
  12. Python如何进行内存管理?
  13. 《高性能JavaScript》第七章 Ajax
  14. 访问数组元素进行获取
  15. 制作css开关,纯css实现开关效果
  16. (六)构建优化(揭开webpack性能优化的内幕)
  17. invoke-rc.d: initscript systemd-logind, action start failed
  18. 在表达式 T(n) = 2T(n/2) + O(1) 与 T(1) = O(1) 中,T(n) 的时间复杂度为多少?
  19. mysql 从后往前截取指定个数字符串_「截取字符串」substring从指定字符串开始截取 - seo实验室...
  20. 百度,高德地图经纬度转换

热门文章

  1. 【iOS】 app 的优化
  2. 【UE4】WebUI插件实现HTML透明区域事件穿透响应
  3. 大道至简(原标题:少是指数级的多)
  4. 计算机 调剂 学校,考研调剂应该怎样联系学校?这三点一定要注意
  5. vue利用 canvas 在图片上加文字
  6. java excel 加密_Java 加密/解密Excel
  7. java读取加密excel_Java 加密和解密Excel文档
  8. 安卓应用加固壳判断java厂商_使用frida来hook加固的Android应用的java层
  9. B站在​港交所双重主要上市 陈睿:将扩大我们投资者基础
  10. js、html实现断点播放视频,视频资源在localStorage中