前言:之前学完ssm框架就想自己搭建一个网站了,但苦于ssm的配置地狱,最近发现boot搭建简易网站的难度要小得多,就着手准备起来了
包括初始化、资源路径文件夹、架构层文件夹、导入thymeleaf依赖并关闭thymeleaf缓存
由于自身的前端框架确实太拉,做不出好看的界面,就直接下载的现成的模板,大概是这样子的,也推荐下这个网站,搜了好多后台模板网站都是收费的,只有这个是公开的站长素材
我是将所有的页面都放进了templates文件夹,将css,js,图片等都放进了static文件夹。
修改后是这样的,th爆红的可能是因为没有加thymeleaf命名空间
跳转静态资源,比如说html网页,需要整合controller,编写一个controller类,然后跳转的URl写成控制mapping就行了
@Controller public class IndexController { @RequestMapping("/toIndex") public String toIndex(){ return "index"; } ..... }
我选的这个网页的公共部分有顶部工具栏、侧边栏和底部,抽取的方式是用thymeleaf的fregment标注id,再用insert注入
静态资源丢失问题,最最恶心的问题之一
我目前遇到的三种情况
第一种,资源路径问题,我开始写的资源路径是对的,因为我清楚的直到boot项目有默认的资源路径,但结果页面演示,就这个功能坑了我,它怎么也加载不出来,我昧着我的良心修改了好多次路径,都不行,在我偶然运行了一次项目后发现成功加载了,顿时人都不好了,这问题也是最经典的boot关于静态资源的问题,我在boot笔记也总结过相关
第二种,thymeleaf缓存问题,项目自身的缓存,当你修改了静态资源,开开心心的去运行项目时,他提交给网页的却是上次加载缓存在jar包里的,所以静态资源修改了但没有完全修改
解决办法是在配置里关掉thymeleaf缓存
spring.thymeleaf.cache=false
第三种,浏览器缓存,和第二种比较像,不过缓存是浏览器帮你做的,真是谢谢了,平时看来很方便的一个功能却成了鸡肋,而且特别恶心人,浏览器提供的清除会清除掉cookie和一些自动帮你填入账号密码的记录,我又不想删除这些,只能选择每次关闭浏览器清除图片和静态资源,每次测试都得关一次浏览器
2022.3.29 时隔多月,我回来了,自从签了工作之后一直在摆烂,最近也是毕设题目刚好是一个网页项目,就想着顺便拿这个做个记录;
浅谈一下新项目吧,新项目是一个会议室预约管理的项目,我也是打算采用springboot去做成一个前后端Web项目,再接着前面的准备工作后,这次我选择了集成springsecurity去认证和授权。
比较令我头疼的是,集成在最开始的测试就失败了(用security的原生登录页面登录不进去,显示账号密码错误),原因我至今还没摸清楚,但是我可以浅谈一下我排除的一些错误
①密码编码问题
security选择将前端输入的密码进行编码后再传输到后端进行逻辑比较(这情况只在高版本出现),那么如果后端的密码是直接从数据库取出来的或者后端直接指定的,那么显然密码比对不会正确,所以要在后端进行密码编码
protected void configure(AuthenticationManagerBuilder auth) throws Exception { //定义一个用户以及他的权限等级 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())//定义前端编码方式 .withUser("amlia"). password(new BCryptPasswordEncoder().encode("123456")).roles("权限1","权限2"); }
②未开启security注解
@EnableWebSecurity //springsecurity的配置类必须加这个注解告诉springboot开启安全模式 //如果security配置类没有被boot识别,还需要再加上@Configuration表明这是一个配置类
③依赖冲突+版本问题
这是我在排除了上面两种可能性之后比较怀疑的,因为我为了后期方便,在项目初始化的时候勾选了很多依赖,但是我后来重新导了一遍冲突,甚至只加了Jdbc和Web的依赖,还是不行,最后我使用之前的MyWeb项目重置了才可以。
在使用Web项目“旧换新”的方法解决了登录问题后,当然是自定义登录页,自定义登录页有一些注意事项,也就是要开始授权,这是初步授权的配置:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.formLogin() .loginPage("/toLogin")//登录页 .loginProcessingUrl("/toLogin")//登陆访问路径 .defaultSuccessUrl("/toHome").permitAll()//登录成功跳转页面 .and().authorizeRequests() //放行登录界面和静态资源 .antMatchers("/","/toHome","login.html","/css/**","/js/**","/images/**").permitAll() .anyRequest().authenticated(); //所有请求都必须被认证,必须登录后才能访问 }
2022.4.10 这个问题在最开始测试登录的时候并没发现,但是最近在测试前端渲染的时候需要重复刷新页面测试,而且我设置了页面拦截。也就是说每次刷新都要重新登录一遍,看起来没问题,但是每次刷新后的第一次登录总是会出问题,报404错误。
最开始我怀疑是不是默认的错误路径/login/error?前面的/login路径和登录路径/login冲突了,我自定义了错误页面并自定义了失败路径.failureUrl("/toError"),但是问题没有得到解决
第一次登录跳转到了一个空白页面,并且路径是/fonts/fontawesome-webfont.ttf?v=4.7.0
我在项目里并没有找到这个路径,我又怀疑是不是缺少了这个文件,我还去fontawesome官网下载了这个文件(真是大冤种)新建了一个对应路径把他放了进去,结果还是不行,一样的错误但是空表页面显示了404,结果我就是给404下载了个样式:-(
在搜索了相关问题后,最后找到了解决办法,原来是重定向的问题,每次刷新之前,比如我之前停留在一个toRoom路径的界面,刷新了之后由于受到了springsecurity的拦截,本应该走向错误界面,但是此时springsecurity将其强制跳转到登录界面,在用户登陆完成之后,默认跳转到了刚才的错误界面,恍然大悟了
要改变这个很简单,只需要在.defaultSuccessUrl("/toHome")后加个true就可以
.defaultSuccessUrl("/toHome",true)
百度是这么说的
.defaultSuccessUrl ("/", true) 如果您未将其设置为 true ,它将自动将用户重定向到上一个上一个请求,在我的情况下,上一个请求将发送到url /error ,这解释了我的问题。 通过将其设置为 true ,您可以强制将用户重定向到您 defaultSuccessUrl 所需要的任何位置。
由于我感觉这个小项目的sql语句可能并不复杂,我使用的是JdbcTemplate
数据库结构初步是这样的
编写对应的映射实体类就行,对于关系表的映射,我使用的是Map集合
private Map<Meetroom,Equipment> belongList
添加数据之后,在test里进行测试,测试出来是成功的,证明数据库连接和Template都是没问题的
@SpringBootTest class MyWebApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() { List<Map<String,Object>> list = jdbcTemplate.queryForList("select * from user"); for (Map<String, Object> map : list) { System.out.println(map.toString()); } } }
之后我编写了对应的dao层接口,最开始,我是这样用的,关系类直接走dao层,然后就用queryForList查找出来一个一个去赋值,meetroomService和userService是一样的
@Data @Component public class Occupy { private Map<Meetroom,User> occupyList; @Autowired MeetroomService meetroomService; @Autowired UserService userService; @Autowired JdbcTemplate jdbcTemplate; public Occupy() { occupyList = new HashMap<>(); List<Map<String,Object>> list = jdbcTemplate.queryForList("select * from occupy"); for (Map<String, Object> map : list) { occupyList.put(meetroomService.findRoomById((int)map.get("rid")), userService.findUserById((int)map.get("eid"))); } } }
但是是行不通的,一直报空指针异常,最开始我以为是 JdbcTemplate对象没有注入成功,但是boot官方文档确实是这么教的,所以我一度很怀疑自己,也怀疑是不是和@Component注解冲突的问题 ,网上确实有只言片语这么说,但是我如果放到测试类里就是好的。证明这个不是问题的关键
这个bug困扰了我许久,在辗转反侧几天后,发现了问题的根源,空指针是语句查询返回空报的,这是springdata的防御机制,为了让程序员的代码更健壮,防止对null值得错误比较,所以是语句的问题,语句需要在加一些约束,不能丢简单的sql,而且为了使代码更具有专业性,我还丢了一个映射类进去,对arm进行解释,这样其实已经有点像Mybatis模式了
@Repository public class EquipmentService { @Autowired JdbcTemplate jdbcTemplate; public List<Equipment> findAllEquip() { return jdbcTemplate.query("select * from equipment", new EquipRowMapper()); } public Equipment findEquipById(int id) {//根据设备id查询到设备 return jdbcTemplate.queryForObject("select * from equipment where eid=?", new Object[]{id}, new EquipRowMapper()); } } class EquipRowMapper implements RowMapper<Equipment> {//映射 @Override public Equipment mapRow(ResultSet rs, int rowNum) throws SQLException { Equipment equipment = new Equipment(); equipment.setEid(rs.getInt("eid")); equipment.setEname(rs.getString("ename")); equipment.setEstate(rs.getBoolean("estate")); return equipment; } }
2022.4.2 在jdbcTemplate方式下的开发遇到了瓶颈,苦于我对jdbcTemplate的映射方式不是很清楚,导致关联表的映射没法进行,是这样的,我有一个用户表和一个会议室表,还有一个额外的表格专门用来记录他们之间的关系,列值就用用户id和会议室号,我想的是返回一个Map<Integer,Integer>来存储比较利于后续的使用,但是我不懂如何用jdbcTemplate去映射,搜寻了一天也无果,好像映射只能用自定义类,像Integer这种没有set方法去设定值得就很麻烦,但是为其专门自定义一个类又很多此一举,倒不如直接把关系表映射到一个实体类上,比如这样
@Data @AllArgsConstructor @NoArgsConstructor public class Occpy { private Map<Integer,Integer> occpyMap; }
但实际上,除此之外,我还有一个关系表用来表示会议室和设备之间的关系,如果要为关系表设置实体类,会显得dao层和service层很臃肿,最终还是决定放弃掉用jdbcTemplate去操作数据库
2022.4.6 经过几天的温习Mybatis和重写架构,新数据库架构已经初步完成,总体思路是修改数据库架构,因为多表子查询对Mybatis也异常复杂,我把关系表改成了多对一绑定,比如一个人可能预约多个会议室,但是会议室同一时间只能被一个人预约,那么在会议室里加一列用户id用来绑定用户,为空时证明会议室无人预约,这样一来只用一个简单的子查询就能解决,而且整个架构也很清晰
简单举例一个UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="www.amlia.com.demo.mapper.UserMapper"> <select id="findUserById" parameterType="int" resultMap="MyUser"> SELECT * FROM user WHERE uid=#{id} </select> <select id="findGradeById" parameterType="int" resultType="Integer"> select ugrade from user where uid=#{id} </select> <select id="findAllUser" resultMap="MyUser"> select * from user meetroom </select> <insert id="addUser" parameterType="User"> insert into user (uid,ugrade,uname,unumber,upassword,phone) values (#{uid},#{ugrade},#{uname},#{unumber},#{upassword},#{phone}) </insert> <update id="updateUser" parameterType="User"> update user set uname=#{uname},unumber=#{unumber},ugrade=#{ugrade} ,upassword=#{upassword},phone=#{phone} </update> <delete id="deleteUser" parameterType="int"> delete from user where uid=#{id} </delete> <select id="findRoomByUser" parameterType="int" resultType="Meetroom"> select * from meetroom where uid=#{id} </select> <!--由于我的数据库列名和类属性名基本一样,这里没进行映射--> <resultMap id="MyUser" type="User"> <collection property="roomList" column="uid" ofType="Meetroom" javaType="java.util.List" select="findRoomByUser"/> </resultMap> </mapper>
2022.4.10 这里解决一下在3.3遇到的问题,我对所有数据库进行了测试,发现id相关的全都输出为0
我检查数据库列名和类属性的确能对应上,因为我当时为了方便,把两个名字定义的一模一样,所以不存在映射不上的问题,我又检查了属性类型发现也确实都是int,随后我发现了,在联合查询下,子查询的结果id是可以显示的,我立马感觉到问题所在了。由于我把属性名和列名定义的一样,所以在联合查询的mapper结果集映射里没有进行映射,按理说可以不用,但是问题出在,我的meetroom表里也存了一个uid,两个冲突了,所以id的映射是必须的
<resultMap id="MyUser" type="User"> <result column="uid" property="uid"></result> <collection property="roomList" column="uid" ofType="Meetroom" javaType="java.util.List" select="findRoomByUser"/> </resultMap>
加了映射之后,输出正确
一般我们都希望将当前所显示的大标题高亮起来,而且会随着我点击不同的标题去动态高亮,这个该如何实现呢,正好这个模板也没有用到高亮,我也去学习了一下相关知识,标题这快的代码是这样的
<div class="collapse navbar-collapse" id="navbarsExample04"> <ul class="navbar-nav mr-auto nav-pills" id="myul"> <!--这个active其实就是高亮,他是vue中的一个元件--> <li class="nav-item active"> <a class="nav-link " th:href="@{/toHome}">首页</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/toLogin}">登录</a> </li> <li class="nav-item"> <a class="nav-link " th:href="@{/toRoom}">会议室</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{gallery.html}">通告</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{blog.html}">历史评价</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{login.html}">我的</a> </li> </ul> </div>
知道了active就是高亮,该怎么让它动态显示呢,这就要用到js去实现,这串代码充分显示了js和java的相似性
$(document).ready(function () { //each是为每一个选中的元素该方法 $("#myul").find("li").each(function () { var a = $(this).find("a:first")[0];//定义一个变量存储当前的第一个a标签,事实上每个li也只有一个标签所以这样写 // location.pathname获取当前浏览器上的url地址,查看与之相同的href即是当前点击的标题 if ($(a).attr("href") == location.pathname) { $(this).addClass("active"); } else { $(this).removeClass("active");//remove为了取消默认首页高亮 } }); });
用js标签直接写到html里或者存在文件里然后引用都可以
2022.4.11因为原先的模板资源有限,并没有我想要的一些页面,比如个人信息页,直接的方式就是再拿一个模板,把想要的html代码插入到我们的页面中。但是这样会带来一个问题,就是css-class命名冲突问题,因为模板的很多命名都是偏系统的,比较相似,冲突问题会导致模板不能呈现出想要的结果,像这样:
就导致了不能单纯引入插入模块的样式,需要有一些手段,最开始我想的比较简单,就是说在div块里有没有一种引用方式可以引用css样式,也就是说,使css样式只针对部分div生效,但是搜寻无果,有一个可能的解决方式是css-moudle,但是碍于我的前端知识过于薄弱,看了半天就好像看天书一般。最后耽搁了几天,选择了一种暴力的方式,就是我把插入模板要引用的css全部整合到一个css里面,然后对其所有引用到的class命进行统一修改,ctrl+f搜索class名,点击这个选中所有就可以统一修改
对于个人信息页面的数据渲染,总体就是用thymeleaf提供的api去实现,非常简单,拿用户姓名的渲染举例,我们需要后端首先获取到当前登录用户的信息,由于我们使用了springsecurity框架,这点变得很容易,只需要定义一个参数Principal principal,这个类对象其实就是springsecurity为我们封装的用户信息
@Autowired UserService userService; @RequestMapping("/toMine") public String toMine(Model model,Principal principal){ List<User> list = userService.findAllUser();//获取一个用户列表 for (User user : list) { if(user.getUnumber().equals(principal.getName())) model.addAttribute("user",user);//找到当前用户,返回给前端 } return "mine"; }
前端方面,直接用th:text="${}"获取文本
<h4 class="card-title m-t-10" th:text="${user.getUname()}"></h4>
如果是循环的话,用th:each,比如我的用户里存的会议室list就可以这么渲染到前端,有点类似于foreach循环
<div th:each="room:${user.getRoomList()}"> <h6 th:text="${room.getRname()}"/><a href="#" class="btn btn-success" th:text="${room.getRgrade()}"></a> <a href="#" class="btn btn-success">取消预订</a><br> <br> </div>
但是在进行渲染头像的时候出了问题,因为头像每个人是不同的,而且要根据uid去动态渲染,thymeleaf也有这种用法,比如在路径里我需要提取uid,就可以这样
<img th:src="@{images/profile/{jpgName}.jpg(jpgName=${user.getUid()%10000})}" class="img-circle" width="150">
我的头像路径是images/profile/*.jpg, 星号代表的是用户uid对10000取余,写法是没问题的,但是无论如何也加载不出来问题,排除了路径、缓存等等问题后,我在前端查看源码发现了,不管用户登录为什么,取余都是0,我起初以为是写法的错误导致传过来为null值然后被初始化了,但是我试了其他的用户等级发现是可以的,我在后台测试了数据库的输出,这才恍然发现,uid从查过来就一直是空的,这个问题我放到数据库那个模块了 ↑,解决之后果然渲染了正确头像
为了先简单的测试一下增删改查的可用性,我新增了一个数据库表blog,blog里存储的是用户的评论,以及通告等,这是几个例子
@Data @AllArgsConstructor @NoArgsConstructor @Component public class Blog { private int replyid;//回复的用户id private int uid;//发表评论的用户id private int bid;//唯一标识一个评论的id private String blog;//发表的评论 private String picture;//发表的图片 private int rid;//评论针对的会议室 private Date date;//发表的时间 }
评论我打算展示在我的页面中,渲染完之后是这样
回复他按钮会新增一个评论,并且指向这个人,具体的前端代码是这样
<div class="tab-pane active" id="home" role="tabpanel"> <div class="card-body"> <div class="profiletimeline"> <div class="sl-item" th:each="replyBolg:${replyBlogs}"> <div class="sl-left"> <img th:src="@{images/profile/{jpgName}.jpg(jpgName=${replyBolg.getUid()%10000})}" alt="user" class="img-circle"> </div> <div class="sl-right" th:with="user1=${userService.findUserById(replyBolg.getUid())}"> <div><a href="#" class="link" th:text="${user1.getUname()}"></a> <span class="sl-date" th:text="${replyBolg.getDate().toString()}"></span> <p th:text="${replyBolg.getBlog()}"> <div class="row"> <div class="col-lg-3 col-md-6 m-b-20"><img th:src="@{images/img1.jpg}" class="img-responsive radius"></div> </div> <form th:action="@{/ReplyToOther}" th:method="post" th:object="${blog}"> <input type="hidden" th:name="uid" th:value="${user.getUid()}"> <input type="hidden" th:name="replyid" th:value="${replyBolg.getUid()}" > <input type="hidden" th:name="date" th:value="${#dates.format(#dates.createNow(),'yyyy-MM-dd')}"> <input placeholder="输入评论" th:name="blog"><button class="btn-primary">回复他</button> </form> </div> </div> </div> <hr> </div> </div> </div>
这里涉及一个知识点,就是themeleaf表单向后端返回bean,th:object去声明这个对象,前提是这个对象要先从后端传到前端来,所以实质上themeleaf只是在修改这个对象的属性,在返回给前端,th:name是这个对象的属性名,th:value是要赋给它的值,要记住,这个对象所对应的类必须要有对应的set方法
Controller层是这样的
@RequestMapping("/ReplyToOther") public String ReplyToOther(Blog blog){ blogService.addBlog(blog); return "redirect:/toMine"; }
这里我遇到了一个问题,就是在数据库里,没有赋值的情况下,int型的初值会是null,但是java里没有赋值的int型是0,也就是说,当我新建了一个回复评论时,他是没有roomid的,回复评论只存储被回复用户的id,也就造成了传到数据库的roomid值为0,但是这个回复评论也需要在我的评论里展示
由于我在我得评论里展示了会议室的名字,所以这个回复评论的roomid查找不出来报错了,也促发了我的另一种想法,用th:if去判断存在性,然后决定是否展出
<label class="col-md-12" th:if="${myBlog.getRid()!=0}" th:text="${room.getRname()}+会议室"></label> <label class="col-md-12" th:if="${myBlog.getReplyid()!=0}" th:with="replyuser=${userService.findUserById(myBlog.getReplyid())}" th:text="回复 +${replyuser.getUname()}"></label>
图片目前还没有解决,涉及更多的问题
我的预约信息是在meetroom表里绑定的,就是每个会议室绑定了一个用户id,所以取消预约实质上是对会议室信息的修改,但是我为了展示方便,在实体类里,用了一个User对象去存储,这也直接导致了修改的困难,总不能是uid=#{user.geiUid()},实不相瞒,我确实试过这个,最终是新加了一个sql解决的,两参数,一个是rid用来精准查找,一个是uid用来修改。
int deleteUser(@Param("uid")int uid,@Param("rid")int rid);
<update id="deleteUser"> update meetroom set uid=#{uid} where rid=#{rid} </update>