Java教程

项目实战——搭建网站(持续更新)

本文主要是介绍项目实战——搭建网站(持续更新),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一步一步copy一个自己的网站

前言:之前学完ssm框架就想自己搭建一个网站了,但苦于ssm的配置地狱,最近发现boot搭建简易网站的难度要小得多,就着手准备起来了

1. 基础搭建

1.1 搭建一个简易的springboot项目

包括初始化、资源路径文件夹、架构层文件夹、导入thymeleaf依赖并关闭thymeleaf缓存

1.2 加入静态资源

由于自身的前端框架确实太拉,做不出好看的界面,就直接下载的现成的模板,大概是这样子的,也推荐下这个网站,搜了好多后台模板网站都是收费的,只有这个是公开的站长素材

image

我是将所有的页面都放进了templates文件夹,将css,js,图片等都放进了static文件夹。

1.3 用thymeleaf和controller渲染所有交互接口

  • 普通静态资源,像js,css,图片这些不需要服务,只需要链接就行,原本是这样的,用thymeleaf接管的好处是它能动态适应路径

image

修改后是这样的,th爆红的可能是因为没有加thymeleaf命名空间

image

  • 跳转静态资源,比如说html网页,需要整合controller,编写一个controller类,然后跳转的URl写成控制mapping就行了

    @Controller
    public class IndexController {
        @RequestMapping("/toIndex")
        public String toIndex(){
            return "index";
        }
        .....
    }      
    

1.4 抽取公共部分

我选的这个网页的公共部分有顶部工具栏、侧边栏和底部,抽取的方式是用thymeleaf的fregment标注id,再用insert注入

image

image

1.5 遇到的问题

  • 静态资源丢失问题,最最恶心的问题之一

    我目前遇到的三种情况

    第一种,资源路径问题,我开始写的资源路径是对的,因为我清楚的直到boot项目有默认的资源路径,但结果页面演示,就这个功能坑了我,它怎么也加载不出来,我昧着我的良心修改了好多次路径,都不行,在我偶然运行了一次项目后发现成功加载了,顿时人都不好了,这问题也是最经典的boot关于静态资源的问题,我在boot笔记也总结过相关

image

第二种,thymeleaf缓存问题,项目自身的缓存,当你修改了静态资源,开开心心的去运行项目时,他提交给网页的却是上次加载缓存在jar包里的,所以静态资源修改了但没有完全修改

解决办法是在配置里关掉thymeleaf缓存

spring.thymeleaf.cache=false

第三种,浏览器缓存,和第二种比较像,不过缓存是浏览器帮你做的,真是谢谢了,平时看来很方便的一个功能却成了鸡肋,而且特别恶心人,浏览器提供的清除会清除掉cookie和一些自动帮你填入账号密码的记录,我又不想删除这些,只能选择每次关闭浏览器清除图片和静态资源,每次测试都得关一次浏览器

2022.3.29 时隔多月,我回来了,自从签了工作之后一直在摆烂,最近也是毕设题目刚好是一个网页项目,就想着顺便拿这个做个记录;

浅谈一下新项目吧,新项目是一个会议室预约管理的项目,我也是打算采用springboot去做成一个前后端Web项目,再接着前面的准备工作后,这次我选择了集成springsecurity去认证和授权。

1.6 集成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项目重置了才可以。

1.7 自定义security登录页

在使用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();
        //所有请求都必须被认证,必须登录后才能访问
    }

1.8 登录问题

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 所需要的任何位置。

2.数据库集成

由于我感觉这个小项目的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>

2.3 遇到的问题

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>

加了映射之后,输出正确

3.前端渲染

3.1 首页标题栏高亮

一般我们都希望将当前所显示的大标题高亮起来,而且会随着我点击不同的标题去动态高亮,这个该如何实现呢,正好这个模板也没有用到高亮,我也去学习了一下相关知识,标题这快的代码是这样的

<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里或者存在文件里然后引用都可以

3.2 插入模板

2022.4.11因为原先的模板资源有限,并没有我想要的一些页面,比如个人信息页,直接的方式就是再拿一个模板,把想要的html代码插入到我们的页面中。但是这样会带来一个问题,就是css-class命名冲突问题,因为模板的很多命名都是偏系统的,比较相似,冲突问题会导致模板不能呈现出想要的结果,像这样:

就导致了不能单纯引入插入模块的样式,需要有一些手段,最开始我想的比较简单,就是说在div块里有没有一种引用方式可以引用css样式,也就是说,使css样式只针对部分div生效,但是搜寻无果,有一个可能的解决方式是css-moudle,但是碍于我的前端知识过于薄弱,看了半天就好像看天书一般。最后耽搁了几天,选择了一种暴力的方式,就是我把插入模板要引用的css全部整合到一个css里面,然后对其所有引用到的class命进行统一修改,ctrl+f搜索class名,点击这个选中所有就可以统一修改

3.3 渲染数据

对于个人信息页面的数据渲染,总体就是用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从查过来就一直是空的,这个问题我放到数据库那个模块了 ↑,解决之后果然渲染了正确头像

4.增删改查

4.1 我的页面

4.1.1 评论系统

为了先简单的测试一下增删改查的可用性,我新增了一个数据库表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>

图片目前还没有解决,涉及更多的问题

4.1.2 取消预约会议室

我的预约信息是在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>
这篇关于项目实战——搭建网站(持续更新)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!