赞
踩
很多年没有从事编程,之前的经验也在C#,从头开始学习Java也是万般无奈。好在经过一个月的学习也算是稍有收获,希望经过这次历程能让自己浴火重生。
在网上看了一段时间的SpringBoot学习视频,感觉不如自己脱离教学独立写一遍CRUD应该会有更多收获,有困难就百度,虽然会慢一些但是效果更好一些。经过一周多的折腾,终于把增删改查写了一遍,页面和样式是网上找的模板。涉及的主要技术点有
以前开发经验主要在后端,前端是依样画葫芦,遇到样式完全就是两眼一抹黑,前端自己写的代码简直没法看,暂时没打算整理,后续有机会再说。
(1)登录
(2)列表
(3)新增/修改
(4)验证插件
(5)删除
(1)idea初始化springboot项目,选择web、mybatis、thymeleaf、mysql,pom基本不需要去修改就可以生成;
(2)生成表结构
SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for department -- ---------------------------- DROP TABLE IF EXISTS `department`; CREATE TABLE `department` ( `id` int(11) NOT NULL AUTO_INCREMENT, `departmentName` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for employee -- ---------------------------- DROP TABLE IF EXISTS `employee`; CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `lastName` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `gender` int(2) DEFAULT NULL, `d_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `login` varchar(50) NOT NULL, `password` varchar(50) NOT NULL, `tel` varchar(50) DEFAULT NULL, `status` tinyint(6) unsigned zerofill DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;
(3)mybatis生成器
生成对应的xml、mapper接口和entity,代码生成器插件
mybatis-generator会生成每个表的criterion查询,对应一个example的类。通过这个类可以构造任意条件的查询。
UserExample example = new UserExample(); UserExample.Criteria criteria = example.createCriteria(); example.setOrderByClause("age asc");//升序 example.setDistinct(false);//不去重 if(!StringUtils.isNotBlank(user.getName())){ Criteria.andNameEqualTo(user.getName()); } if(!StringUtils.isNotBlank(user.getSex())){ Criteria.andSexEqualTo(user.getSex()); } List<User> userList=userMapper.selectByExample(example); //等同:select * from user where name={#user.name} and sex={#user.sex} order by age asc;
按名字模糊查询
if(!StringUtils.isNotBlank(user.getName())){
criteria.andNameLIke(‘%’+name+’%’);
}
List<User> userList=userMapper.selectByExample(example);
如果是自动生成的xml,一般情况下是没有问题,检查以下4点;
另外,就是方法名要和mapper里的接口名称要一模一样,包括大小写
数据库连接,还有xml所在路径(mybatis.mapper-locations)
#tomcat server.port=8080 server.tomcat.uri-encoding=utf-8 #http spring.http.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true #datasource spring.datasource.tomcat.driver-class-name = com.mysql.jdbc.Driver #mybatis spring.datasource.url=jdbc:mysql://192.168.73.128:3306/mydb?useUnicode=true&characterEncoding=UTF-8&transformedBitIsBoolean=true&autoReconnect=true&failOverReadOnly=false&allowMultiQueries=true&useSSL=false spring.datasource.username=dyhu spring.datasource.password=123456 spring.datasource.tomcat.default-auto-commit=true mybatis.typeAliasesPackage=com.demo.crud.dao mybatis.mapper-locations=classpath:mapping/*.xml
@MapperScan("com.demo.crud.dao")
@SpringBootApplication
public class CrudApplication {
public static void main(String[] args) {
SpringApplication.run(CrudApplication.class, args);
}
}
一种是直接修改setting +++>Editor+++>Inspections+++>Spring+++>SpringCore+++>Code+++>Autowiring For Bean Class
把报警去掉。不过不建议这么做,如果万一真的代码写错,一直到运行时才报错就麻烦了。
另一种解决方案:
如果是mapping的接口在注入时,加上required=false即可
@Autowired(required = false)
private UserDao userDao;
头部需要加入注解,否则有可能会导致空指针错误,无法注入mapper接口
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
UserService service;
@Test
public void getUsersByPages() {
List<User> list = service.getUsersByPages(1, 2);
String result = list.toString();
System.out.println(result);
}
}
mybatis中的mapper接口,没法做实例化,只能通过@Autowired进行注入到spring容器,前提是该类用@Service注解
另外,如果有实例对象中的方法用到了其他对象,该对象也同事用到了mybatis的mapper接口,该对象也需要用@Autowired注入,用new方法,会导致空指针。
比如
@Service public class EmployeeService { @Autowired(required = false) EmployeeDao employeeDao; @Autowired EmployeeBiz employeeBiz; //部门列表查询 public PageInfo<EmployeeBO> selectByPage(int pageNum, int pageSize) { //Example、Dao,包括selectByExample方法都是代码生成器生成 EmployeeExample example = new EmployeeExample(); PageHelper.startPage(pageNum, pageSize); //selectByExample(example),因为没有指定任何查询条件,正常是返回所有数据 //在这里指定PageHelper拦截了sql,加上了limit参数 List<Employee> emps = employeeDao.selectByExample(example); //需要更新部门,!注意这个employeeBiz对象,里面也用到了mapper接口 List<EmployeeBO> empBOs = employeeBiz.getEmpBOsByPO(emps); //如果直接返回以上list,得到了分页的数据 // 如果添加下面步骤,则返回pageInfo,则能得到包括list在内的分页信息 PageInfo<EmployeeBO> pageInfo = new PageInfo<>(empBOs); return pageInfo; }
employeeBiz.getEmpBOsByPO
@Service
public class EmployeeBiz {
@Autowired
DepartmentBiz departmentBiz;
public List<EmployeeBO> getEmpBOsByPO(List<Employee> emps) {
if (emps == null || emps.size() == 0) return null;
List<Integer> ids = new ArrayList<>();
for (Employee emp : emps
) {
ids.add(emp.getdId());
}
Map<Integer, Department> deptlist = departmentBiz.getDeptlistByIds(ids);
而这个departmentBiz.getDeptlistByIds(ids)就用到了mapper的接口
public Map<Integer, Department> getDeptlistByIds(List<Integer> ids) {
DepartmentExample example = new DepartmentExample();
if (ids.size() == 0) return null;
example.createCriteria().andIdIn(ids);
List<Department> departments = deptDao.selectByExample(example);
Map<Integer, Department> depts = new HashMap<>();
for (Department dept: departments
) {
depts.put(dept.getId(), dept);
}
return depts;
}
@Service的说明
主要是针对@Autowired,注入的是接口类,不需要再new实现类
把DAO实现类注入到action的service接口(注意不要是service的实现类)中,注入时不要new 这个注入的类,因为spring会自动注入,如果手动再new的话会出现错误,然后属性加上@Autowired后不需要getter()和setter()方法,Spring也会自动注入。
在接口前面标上==@Autowired注释使得接口可以被容器注入==,如:
@Autowired
@Qualifier("cn")
private User user;
当接口存在两个实现类的时候必须使用@Qualifier指定注入哪个实现类,否则可以省略,只写@Autowired。
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.7</version>
</dependency>
properies文件加上:
# 将mapper接口所在包的日志级别改成debug,可以在控制台打印sql
logging.level.com.demo.crud.dao: debug
@Autowired UserDao userDao; @GetMapping("/page/{pageNum}/{pageSize}") public PageInfo<User> selectByPage(@PathVariable int pageNum, @PathVariable int pageSize) { //Example、Dao,包括selectByExample方法都是代码生成器生成 UserExample example = new UserExample(); PageHelper.startPage(pageNum, pageSize); //selectByExample(example),因为没有指定任何查询条件,正常是返回所有数据 //在这里指定PageHelper拦截了sql,加上了limit参数 List<User> list = userDao.selectByExample(example); //如果直接返回以上list,得到了分页的数据 // 如果添加下面步骤,则返回pageInfo,则能得到包括list在内的分页信息 PageInfo<User> pageInfo = new PageInfo<>(list); return pageInfo; }
返回的PageInfo
{ "total": 6, "list": [ { "id": 6, "lastname": "鬼脚七", "email": "gu@gu.com", "gender": 1, "dId": 4 }, { "id": 5, "lastname": "李娜", "email": "ln@ab.cn", "gender": 0, "dId": 2 }, { "id": 4, "lastname": "赵柳", "email": "zl@tp.com", "gender": 1, "dId": 3 }, { "id": 3, "lastname": "王五", "email": "ww@de.com", "gender": 0, "dId": 2 }, { "id": 2, "lastname": "李四", "email": "ls@de.com", "gender": 1, "dId": 1 }, { "id": 1, "lastname": "张三", "email": "zs@de.com", "gender": 1, "dId": 1 } ], "pageNum": 1, "pageSize": 20, "size": 6, "startRow": 1, "endRow": 6, "pages": 1, "prePage": 0, "nextPage": 0, "isFirstPage": true, "isLastPage": true, "hasPreviousPage": false, "hasNextPage": false, "navigatePages": 8, "navigatepageNums": [ 1 ], "navigateFirstPage": 1, "navigateLastPage": 1 }
这些返回信息对应前端意义还是很大的
还有Debug打印出的SQL信息
① 前端或者restful api入口
②直接指向模板页(也可以在config中,定义controller拦截器,直接定义模板入口);
③参数处理和非空判断;
④权限判断/or拦截器处理;
⑤按照前端要求返回数据处理(比如把service传过来的bo对象,转换成vo);
系统简单的话,可以和biz合并。实质上可以进行细分
只对controller开放的方法
包括数据组装,获取业务对象(bo),分页处理
查询条件的组装
可以直接访问dal层
具体业务逻辑的实现,业务规则细化,简单的系统完全可以在service层面实现。
简单的crud无需存在。
可以细分
又可以细分为
html需要导入thymeleaf命名空间,支持thymeleaf语法
<html lang="en" xmlns:th="http://www.thymeleaf.org">
@Controller public class HelloController { @ResponseBody @RequestMapping("/hello") public String hello() { return "hello world!"; } /* @RequestMapping("/success") public Map success(Map<String, Object> map) { map.put("hello", "haha .....!"); return map; }*/ @RequestMapping("/success") public String success(Map<String,Object> map){ map.put("hello","<h1>------你好------------</h1>"); //map.put("users", Arrays.asList("zhangsan","lisi","wangwu")); return "success"; } }
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>success</title>
</head>
<body>
<h1>成功!</h1>
<!--th:text 将div里面的文本内容设置为 -->
<div th:text="${hello}">这是显示欢迎信息</div>
<div th:utext="${hello}">这是显示欢迎信息</div>
</body>
</html>
controller中方法返回值要和thymeleaf的html对应
@PostMapping("/login/check") public String doLogin(@RequestParam("userName") String userName, @RequestParam("password") String password, Map<String, Object> result) { if (!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password)) { User user = new User(); user.setLogin(userName); user.setPassword(password); if (userService.doLogin(user)) { return "redirect:/users/1"; } } result.put("errmsg", "用户名或者密码错误"); return "login"; }
其中return “login”,在resouses/templates下有一个login.html对应
目的是自定义解析html入口,如果入口仅仅是一个简单路由或者一个转发,不需要在controller定义方法
@Configuration public class WebMVCAutoConfigure implements WebMvcConfigurer { //必要! @Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); // 设置thymeleaf的解析器 viewResolver.setPrefix("classpath:/templates/"); viewResolver.setSuffix(".html"); return viewResolver; } //必要!定义statics可以扫描的路径,否则系统无法访问该目录404 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/statics/"); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); registry.addViewController("index.html").setViewName("login"); registry.addViewController("index").setViewName("login"); registry.addViewController("login").setViewName("login"); registry.addViewController("myvalid").setViewName("myvalid"); registry.addViewController("validator").setViewName("validator"); } }
pom引入
<!--引入bootstrap-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.0.0</version>
</dependency>
css样式引入
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
js引入
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
说实话,实际项目中的应用到thymeleaf场景不多,即使有也只是html初始渲染会用到后端提供的数据,毕竟现在大部分都是异步无刷新查询,还有大量的前端框架可以使用。个人觉得只要了解一些基本语法和使用方法就可以了。
我在这个demo用到的有
th属性
实例
引入js和css
<script src="js/jquery.min.js" th:src="@{/statics/js/jquery.min.js}"></script>
<script src="js/bootstrap.min.js" th:src="@{/statics/js/bootstrap.min.js}"></script>
@{},用于th:src或者th:href,替换链接,会自动加上context
引入公共页面
<!--引入左侧菜单-->
<div th:replace="common/leftmenu::leftmenu"></div>
common/leftmenu,是指公共页面所在目录和名称
::leftmenu,双冒号后面就是要引入的代码块
<!-- 左侧菜单栏目块 --> <div class="leftMeun" id="leftMeun" th:fragment="leftmenu"> <div id="logoDiv"> <p id="logoP"><img id="logo" alt="CRUD-Test" src="images/logo.png" th:src="@{/statics/images/logo.png}"><span>CRUD-Test-Demo</span></p> </div> <div id="personInfor"> <p> <p id="userName"></p> <p></p> <p> <a>退出登录</a> </p> </p> </div> <div class="meun-title">账号管理</div> <div class="meun-item meun-item-active" href="#user" aria-controls="user" role="tab" data-toggle="tab"><img src="images/icon_user_grey.png" th:src="@{/statics/images/icon_user_grey.png}">用户管理</div> <div class="meun-item" href="#chan" aria-controls="chan" role="tab" data-toggle="tab"><img src="images/icon_change_grey.png" th:src="@{/statics/images/icon_change_grey.png}">修改密码</div> </div>
循环
<div class="tablebody" th:each="user:${users.list}"> <div class="row"> <div class="col-lg-3 col-md-3 col-sm-3 col-xs-3 " th:text="${user.login}"> </div> <div class="col-lg-3 col-md-3 col-sm-3 col-xs-3" th:text="${user.name}"> 李豆豆 </div> <div class="col-lg-2 col-md-2 col-sm-2 col-xs-2" th:text="${user.tel}"> 13688889999 </div> <div class="col-lg-2 col-md-2 col-sm-2 col-xs-2" th:text="${user.status} == 1? '启用':'禁用'"> 状态 </div> <div class="col-lg-2 col-md-2 col-sm-2 col-xs-2"> <button class="btn btn-success btn-xs" data-toggle="modal" data-target="#reviseUser" th:attr="userid=${user.id},currpage=${users.pageNum}">修改</button> <!--delete url: /user/{id}/{currentpage} --> <button class="btn btn-danger btn-xs" data-toggle="modal" data-target="#deleteUser" th:attr="del_url=@{/user/}+${user.id}+'/page/'+${users.pageNum}">删除</button> </div> </div> </div>
th:each=“user:${users.list}”,这里会根据后端提供的变量users.list进行循环,有多少行就会显示多少个div标签
并把每个循环元素赋值一个行变量user,通过th:text="${user.属性名}",显示到对应的html标签
条件判断
<!--th:if 条件判断,类似的有th:switch,th:case,优先级仅次于th:each, 其中#strings是变量表达式的内置方法-->
<p th:text="${thIf}" th:if="${not #strings.isEmpty(thIf)}"></p>
根据thymeleaf提供的内部函数strings来判断thIf是否为空,如果为空则当前p标签不展示,否则就显示thIf内容
样式都是固定的写法,抄着写就行了,定义一个要弹出的div
<!--弹出删除用户警告窗口--> <div class="modal fade" id="deleteUser" role="dialog" aria-labelledby="gridSystemModalLabel"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> <h4 class="modal-title" id="gridSystemModalLabe">提示</h4> <input type="hidden" id="del_url"/> </div> <div class="modal-body"> <div class="container-fluid"> 确定要删除该用户?删除后不可恢复! </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-xs btn-white" data-dismiss="modal">取 消</button> <button type="button" class="btn btn-xs btn-danger" onclick="deleteUser()">确 定</button> </div> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal -->
调用方,button的目标写上div的id即可
<button class="btn btn-danger btn-xs" data-toggle="modal" data-target="#deleteUser" th:attr="del_url=@{/user/}+${user.id}+'/page/'+${users.pageNum}">删除</button>
$('#deleteUser').on('show.bs.modal', function (event) {
//打开时的事件,比如需要做初始化
});
$("#deleteUser").on('hidden.bs.modal',function(e) {
//关闭时的事件
});
<script src="js/jqPaginator.min.js" th:src="@{/statics/js/jqPaginator.min.js}"></script>
<!--页码块-->
<div>
<ul class="pagination media-left pull-right" id="pagination">
</ul>
<input type="hidden" id="PageCount" th:value="${users.pages}" />
<input type="hidden" id="PageSize" value=20 th:value="${users.pageSize}" />
<input type="hidden" id="countindex" value="10"/>
<input type="hidden" id="currentPage" value=1 th:value="${users.pageNum}" />
<!--设置最多显示的页码数 可以手动设置 默认为-->
<input type="hidden" id="visiblePages" value="10" />
</div>
function loadpage() { var myPageCount = parseInt($("#PageCount").val()); var myPageSize = parseInt($("#PageSize").val()); var countindex = myPageCount % myPageSize > 0 ? (myPageCount / myPageSize) + 1 : (myPageCount / myPageSize); $("#countindex").val(countindex); $.jqPaginator('#pagination', { totalPages: parseInt($("#countindex").val()), visiblePages: parseInt($("#visiblePages").val()), currentPage: parseInt($("#currentPage").val()), first: '<li class="first"><a href="javascript:;">首页</a></li>', prev: '<li class="prev"><a href="javascript:;"><i class="arrow arrow2"></i>上一页</a></li>', next: '<li class="next"><a href="javascript:;">下一页<i class="arrow arrow3"></i></a></li>', last: '<li class="last"><a href="javascript:;">末页</a></li>', page: '<li class=""><a href="javascript:;">{{page}}</a></li>', onPageChange: function (num, type) { if (type == "change") { exeData(num, type); } } }); } function loadData(num) { //非异步加载数据,不需要传入页号num $("#PageCount").val([[${users.total}]]); loadpage(); } function exeData(num, type) { // loadData(num); // loadpage(); //var context = [[@{/users/}]]; location.href = "users/" + num; // [[${users.pageNum}]]; } $(function () { //异步刷新,页面重刷传入第一页 loadData(1); //loadpage(); });
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jquery.bootstrapvalidator/0.5.2/css/bootstrapValidator.min.css"/>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery.bootstrapvalidator/0.5.2/js/bootstrapValidator.min.js"></script>
把需要验证的数据都放在一个form表单中
这里只是做简单验证,验证规则还可以添加
初始化验证器,验证器中的字段(field)要和form表单中的input对应:
function initValid() { $('#updateForm').bootstrapValidator({ message: '这个值没有被验证', feedbackIcons: { valid: 'glyphicon glyphicon-ok', invalid: 'glyphicon glyphicon-remove', validating: 'glyphicon glyphicon-refresh' }, fields: { /*验证:规则*/ login: { //验证input项:验证规则 message: 'The username is not valid', validators: { notEmpty: { message: '用户名不能为空' } } }, name: { message: 'name无效', validators: { notEmpty: { message: '用户名不能为空' } } }, password: { message: '密码无效', validators: { notEmpty: { message: '密码不能为空' }, stringLength: { min: 4, message: '密码长度须大于等于4位' } } } } }); }
销毁验证器:
function destroyValid() {
var validator = $("#updateForm").data('bootstrapValidator');
if (validator != undefined) {
$("#updateForm").data('bootstrapValidator').destroy();
$('#updateForm').data('bootstrapValidator', null);
}
}
我这里在模态框打开时初始化验证器,关闭模态框时销毁验证器
$('#reviseUser').on('show.bs.modal', function (event) { var button = $(event.relatedTarget) // Button that triggered the modal var userId = button.attr('userid'); $("input:hidden[id='currpage']").val(button.attr('currpage')); $("input:hidden[id='id']").val(userId); initUser(userId, $(this)); //初始化验证器 initValid(); }); $("#reviseUser").on('hidden.bs.modal',function(e) { //关闭modal时,移除上次的校验配置 destroyValid(); });
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。