title: ssm框架学习笔记
date: 2018-03-21 16:49:52
updated: 2020-03-12 14:34:23
categories: spring
tags:
- spring
此文档记录研二学习ssm框架的知识点笔记
SSM简介
最初:jsp + servlet + jdbc
现在:springmvc + spring + mybatis
官网地址:
logback配置:https://logback.qos.ch/manual/configuration.html
spring-api-doc:https://docs.spring.io/spring/docs/
mybatis中文官网:http://www.mybatis.org/mybatis-3/zh/index.html
Bean容器初始化
一、Spring常用的三种注入方式
Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:setter设值注入,构造注入,基于注解的注入。
基于注解的注入
基于注解的注入:注解方式注册bean,注入依赖
- 主要有四种注解可以注册bean,每种注解可以任意使用,只是语义上有所差异:
@Component:可以用于注册所有bean
@Repository:主要用于注册dao层的bean
@Controller:主要用于注册控制层的bean
@Service:主要用于注册服务层的bean
- 描述依赖关系主要有两种:
@Resource:java的注解,默认以byName的方式去匹配与属性名相同的bean的id,如果没有找到就会以byType的方式查找,如果byType查找到多个的话,使用@Qualifier注解(spring注解)指定某个具体名称的bean。
```
@Resource
@Qualifier(“userDaoMyBatis”)
private IUserDao userDao;
public UserService(){
}
@Autowired:spring注解,默认也是以byName的方式去匹配与属性名相同的bean的id,如果没有找到,就通过byType的方式去查找,如果查找到多个,用@Qualifier注解限定具体使用哪个。
@Autowired @Qualifier(“userDaoJdbc”)
private IUserDao userDao;
```
二、spring学习之AOP
入门请参考:Spring AOP入门Demo
、Java Spring AOP用法
AOP实现方式
1.预编译
- AspectJ
2.运行期动态代理(JDK动态代理、CGLib动态代理)
- SpringAOP、JbossAOP
Schema-based AOP(基于XML配置实现的SpringAOP)
Spring 所有的切面和通知器都必须放在一个<aop:config>内(可以包含多个<aop:config>元素),每一个<aop:config>可以包含pointcut, advisor和aspect元素(它们必须按照这个顺序声明)
<bean id="car" class="com.braincao.aop.car.Car"></bean>
<bean id="carLogger" class="com.braincao.aop.car.CarLogger"></bean>
<aop:config>
<!--指定切面-->
<aop:aspect id="carLoggerAOP" ref="carLogger">
<!--定义切点-->
<aop:pointcut id="go" expression="execution(* com.braincao.aop.car.Car.go(..))"></aop:pointcut>
<!--定义连接点-->
<aop:after-returning method="beforeRun" pointcut-ref="go"></aop:after-returning>
<aop:after method="afterRun" pointcut-ref="go"></aop:after>
</aop:aspect>
</aop:config>
特点
由Spring创建了一个car对象。Spring在创建该对象时,发现它的一个方法被配置成了切点(pointcut),所以,在实例化该对象时,会创建一个代理对象,当切点方法go()执行时,会被Spring创建的代理对象所拦截,运行go方法之前,会调用所对应的切面类CarLogger的前置方法beforeRun(),然后调用Car.go()方法,再然后就调用切面类CarLogger的后置方法afterRun()。
基于注解实现的SpringAOP
略
看完关于ioc,bean装载和aop之后的思考
IOC即DI,不必调用者自己去new被调用对象,而是通过spring IOC容器把配置好的bean对象注入,可以通过设置注入即setter方法和构造器注入。bean装载可以通过xml配置设定,也可以同过设定扫描路径,然后通过注解来让容器识别到要装载的bean。aop面向切面编程,切面与业务是垂直的,不同业务往往都要做一些公共的类似的额外操作,在业务之前做,或在业务之后做,或在业务出了异常时做,或者在业务前后都要做,甚至这些要做的额外操作要用到业务本身的输入参数和业务完成的输出结果。比如业务一般都得记录日志,比如涉及数据更新的业务完成后都得伴随数据库操作,账户各种操作前都要验证用户权限,这些业务伴随的操作往往大致相似,如果每个业务都要写这些操作,特别繁琐,把这些操作提出来就成了切面,与业务分离。xml 和API方式都可以实现aop配置,pointcut是业务,aspect是切面,它俩怎么交互执行,怎么传参和调用结果,都可以通过xml和API方式实现。另外还有配置代理这一块比较蒙逼。最牛逼的是,之前看得傻了眼那么繁琐和复杂的xml,api方式用简单直观的aspectj方式竟然能等效实现,用的纯Java标签,在xml 里设一下自动代理。不过仅仅@Aspect容器不识别,要加上@Component 才识别。我觉得标签简直就是福音,差点我就被吓的放弃了。我以为一辈子就只能写xml了。orz。。。
1、若代理类有接口,使用JDK代理。也可以通过设置proxyTargetClass为true,强制使用CGLIB代理
1、若代理类无接口,使用CGLIB代理
3、如果proxyInterfaces属性被设置为一个或者多个全限定接口名,则使用JDK代理;如果该属性没有被设置,但是目标类实现了接口,也使用JDK代理。
三、Spring事务管理
事务:指的是逻辑上的一组操作,这组操作要么全部成功,要么全部失败
Spring事务管理主要包含3个接口
-
PlatformTransactionManager–事务管理器
-
TransactionDefinition–事务定义信息(隔离、传播、超时、只读)
-
TransactionStatus–事务具体运行状态
以上三个接口详细看Spring api即可,下面简要介绍几个重点知识。
PlatformTransactionManager–事务管理器
TransactionDefinition–事务定义信息(隔离、传播、超时、只读)
事务的传播行为:web层->业务层(Service)->持久层(DAO)
TransactionStatus–事务具体运行状态
略
Spring事务管理
spring支持两种方式事务管理:
-
编程式事务管理
在实际应用中很少使用,通过TransactionTemplate手动管理事务
-
使用XML配置声明式事务
开发中推荐使用(代码侵入性最小),spring的声明式事务是通过AOP实现的
spring事务实践–转账案例
具体可看 转账案例
声明式事务管理的三种方式
详情参考声明式事务管理的三种方式
声明式事务管理的三种方式:基于TransactionProxyFactoryBean的方式(很少使用),基于AspectJ的配置方式(经常),以及基于注解的方式(经常)。
基于TransactionProxyFactoryBean的方式
基于TransactionProxyFactoryBean的方式不常用,因为要对需要事务的每个类都设置一个代理类,繁琐
基于AspectJ的配置方式
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--配置连接池-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务的通知(事务增强)(基于aspectj声明式事务管理)-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
propagation :传播事务行为
isolation :事务隔离级别
read :只读
rollback-for :发生哪些异常回滚
no-rollback-for :发生哪些异常不回滚
-->
<tx:method name="transfer" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--配置aop-->
<aop:config>
<!--配置切入点 AccountService+表示所有其子类-->
<aop:pointcut id="pointcut" expression="execution(* transactiondemo02.service.AccountService+.*(..))"></aop:pointcut>
<!--配置切面:表示在pointcut切入点上应用txAdvice增强-->
<!--advisor表示有一个切入点的,aspect表示有多个切入点的-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
基于注解的事务管理配置方式
很明显这个更简单啊!
xml:
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--配置连接池-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启注解事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
ServiceImpl:
//注解式的事务管理
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT))
public class AccountServiceImpl implements AccountService {
@Transactional的属性:
```
propagation :传播事务行为
isolation :事务隔离级别
readOnly :只读
rollbackFor :发生哪些异常回滚
noRollbackFor :发生哪些异常不回滚
## 四、Spring MVC
mvc: Model-Viewer-Controller
mvc的核心思想是业务数据**抽取**同业务数据**呈现**相**分离**。
前端控制器Front Controller(MVC)
![1583754122](https://img.braincao.cn/blogimg/1583754122.jpg)
Spring MVC为我们提供了一个基于组件和松耦合的MVC实现框架。在使用Java中其它MVC框架多年之后,面对Spring MVC有一种相见恨晚的感觉。Spring MVC是如此的优雅,轻盈与简洁, 让人从其它框架的桎梏解脱出来。
SpringMVC是一个基于DispatcherServlet的MVC框架,每一个请求最先访问的都是DispatcherServlet,DispatcherServlet负责转发每一个Request请求给相应的Handler,Handler处理以后再返回相应的视图(View)和模型(Model),返回的视图和模型都可以不指定,即可以只返回Model或只返回View或都不返回。SpringMVC是基于DispatcherServlet的,DispatcherServlet是继承自HttpServlet的,HttpServlet是在web.xml文件中声明的。(相关配置请左转至: maven_pom等相关配置)
![1583754147](https://img.braincao.cn/blogimg/1583754147.png)
如图,其中我们最主要写的就是Controller,其他的基本都是框架的东西。
### Spring MVC具体的项目实践
详情参考下面,重点学习了四大块:
- 基本的controller编写
- 数据绑定
- 文件上传
- json协同
视频:[Spring MVC实操](https://www.imooc.com/video/7681)
视频太慢并且已经看完,复习直接看这个人的同步笔记即可[Spring MVC起步](https://www.cnblogs.com/zjfjava/p/6746704.html)
这个视频我自己学习实践的同步工程也已上传到github了,直接看自己的项目工程即可**[springmvc_demoproject](https://github.com/braincao/springmvc_demoproject)**
注意:项目工程的WEB-INF下所有文件是私有的,必须经过后端调用才能访问到的,如果要公共访问的东西需要放在webapps目录下
### controller三种方式
controller的多种操作:基本的controller编写、数据绑定、文件上传、json协同
```java
package com.braincao.controller;
import com.braincao.model.Course;
import com.braincao.service.CourseService;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
@Controller
@RequestMapping("/courses")
public class CourseController {
private CourseService courseService;
@Autowired
public void setCourseService(CourseService courseService) {
this.courseService = courseService;
}
//法一:本方法将处理http://localhost:8080/courses/view?courseId=123
//此方法url中必须指定?courseId=xxx的参数名courseId,参数名不可自适应改变,不好
// @RequestParam 和@PathVariable绑定了请求中的参数。@RequestParam("courseId")显式指明参数
@RequestMapping(value = "/view", method = RequestMethod.GET)
public String viewCourse(@RequestParam("courseId") Integer courseId, Model model){//Model就是spring mvc的部分
Course course = courseService.getCoursebyId(courseId);
model.addAttribute(course);//将查询到的course放到model中
return "course_overview";//返回jsps目录下的course_overview.jsp
}
//法二:这个方法就不用指定参数名courseId了,自动识别/view2/后面的东西并放到mvc中
@RequestMapping(value = "/view2/{courseId}", method = RequestMethod.GET)
public String viewCourse2(@PathVariable("courseId") Integer courseId, Map<String, Object> model){//Model就是spring mvc的部分
Course course = courseService.getCoursebyId(courseId);
model.put("course", course);//将查询到的course放到model中
return "course_overview";//返回jsps目录下的course_overview.jsp
}
//法三:上面两种方法都是spring mvc方法,下面这个是传统的HttpServletRequest方法
//处理/courses/view3?courseId=123形式的url
@RequestMapping(value = "/view3", method = RequestMethod.GET)
public String viewCourse3(HttpServletRequest request){
Integer courseId = Integer.valueOf(request.getParameter("courseId"));
Course course = courseService.getCoursebyId(courseId);
request.setAttribute("course", course);//将查询到的course放到model中
return "course_overview";//返回jsps目录下的course_overview.jsp
}
//绑定binding:将请求中的字段(前端)按照名字匹配原则填入模型对象(后端)
//处理/courses/create?add形式的url来创建表单,表示一个参数params = "add"
@RequestMapping(value = "/create", method=RequestMethod.GET, params = "add")
public String createCourse(){
return "admin_create/edit";//二级目录也能行哦
}
//绑定binding法一:不用@ModelAttribute
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String doSave(Course course){
//这里进行业务操作,比如数据库持久化
course.setCourseId(456);
return "redirect:view2/" + course.getCourseId();//请求重定向:因为这里提交表单后courseId更新了,需要请求重定向,这样就完成更新
}
//绑定binding法二:用@ModelAttribute也可以
@RequestMapping(value = "/save2", method = RequestMethod.POST)
public String doSave2(@ModelAttribute Course course){
//这里进行业务操作,比如数据库持久化
course.setCourseId(456);
return "redirect:view2/" + course.getCourseId();//请求重定向:因为这里提交表单后courseId更新了,需要请求重定向,这样就完成更新
}
//文件上传的controller
@RequestMapping(value = "/upload", method = RequestMethod.GET)
public String showUploadPage(){
return "admin_upload/edit";
}
@RequestMapping(value = "/doUpload", method = RequestMethod.POST)
public String showUploadPage(@RequestParam("file") MultipartFile file) throws IOException {//file从页面上来
if(!file.isEmpty()){//无损的(InputStream)将上传的文件保存到指定目录下,并返回success界面
FileUtils.copyInputStreamToFile(file.getInputStream(), new File("/Users/braincao", System.currentTimeMillis()+file.getOriginalFilename()));
}
return "admin_upload/success";
}
//JSON格式的前后端交互之一:给定courseId,返回对应课程的json数据,给前端展示
@RequestMapping(value="/{courseId}", method = RequestMethod.GET)
public @ResponseBody Course getCourseInJson(@PathVariable Integer courseId){
return courseService.getCoursebyId(courseId);
}
//上面方法的第二种实现(泛型实现)。这两种都可以
@RequestMapping(value="/jsontype/{courseId}", method = RequestMethod.GET)
public ResponseEntity<Course> getCourseInJson2(@PathVariable Integer courseId){
Course course = courseService.getCoursebyId(courseId);
return new ResponseEntity<Course>(course, HttpStatus.OK);
}
//JSON格式的前后端交互之二:异步方式获取数据,前端通过js代码(ajax)来整合页面,
// 即前端写个框架本来没数据,运行时异步加载获取数据填充到界面中,
// 且注意前端的这个加载页面一定要放在webapps下,WEB-INF是私有的不能访问
// 现在用上面两个已实现的json交互方法进行:前端异步加载后端json数据的demo。给定json数据,返回对应课程的信息,前端展示信息界面
//启动tomcat后直接浏览器访问http://localhost:8080/courses_json.jsp?courseId=123即可
}
Ajax异步获取服务端json数据并动态加载前端页面
启动tomcat后直接浏览器访问http://localhost:8080/courses_json.jsp?courseId=123即可
webapps目录下的courses_json.jsp:
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><!--将jstl标签库引入此.jsp文件,简化jsp开发-->
<!DOCTYPE html PUBLIC "-//W3C//DTD//XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>前端异步加载后端json数据的demo</title>
<%--这个css样式我没有,展示就简朴一点吧<link rel="stylesheet" href="<%=request.getContextPath()%>/resources/css/main.css" type="text/html">--%>
<!--引入本地jQuery-->
<script type="text/javascript" src="<%=request.getContextPath()%>/resources/js/jquery-1.11.3.min.js"></script>
</head>
<script>
jQuery(function($){
var urlStr = "<%=request.getContextPath()%>/courses/<%=request.getParameter("courseId")%>";
//alert("Before Call: " + urlStr);
$.ajax({
//通过ajax异步从服务端拿到json数据并动态加载页面
method: "GET",
url: urlStr,
success: function(data, status, jqXHR){
//alert("Success: " + data);
var course = data;
var path = "<%=request.getContextPath()%>/";
$(".course_title").text(course.courseTitle);
$(".course_info").text(course.courseDesc);
$(".course_imgPath").attr("src", path+course.imgPath);
$("#course_duration").text(course.courseDuration);
}
});//end ajax
});
</script>
<body>
<p>This is MvcHome, your world!</p>
<h3 class="course_title"></h3>
<h3 class="course_info"></h3>
<h3 id="course_duration"></h3>
<div><img class="course_imgPath"/></div>
</body>
</html>
springmvc拦截器
拦截器是指通过统一拦截从浏览器发往服务器的请求来完成功能的增强,解决请求的共性问题(乱码、权限验证等问题)
springmvc过滤器也能起到一定的拦截器作用
拦截器的实现:
- 1.编写拦截器类实现HandlerInterceptor接口(WebRequestInterceptor接口也可以,但不常用)
- 2.将拦截器注册进SpringMVC框架中(mvc-dispatcher-servlet.xml)
- 3.配置拦截器的拦截规划
过滤器与拦截器的区别:
-
过滤器Filter依赖于Servlet容器,基于回调函数,过滤范围大
-
拦截器Interceptor依赖于框架容器,基于反射机制,只拦截请求
MyBatis入门
mybatis的特点
1.sql语句与代码分离。优点:便于管理和维护;缺点:不便于调试,需要借助日志工具获得信息
2.用标签控制动态sql语句的拼接。优点:用标签代替编写逻辑代码;缺点:拼接复杂sql语句时,没有代码灵活,比较复杂
3.结果集与java对象的自动映射。优点:保证名称相同可自动映射;缺点:对开发人员所写的sql依赖性很强
4.编写原生sql。优点:接近JDBC,很灵活;缺点:对sql语句依赖很高(不同数据库sql语句可能不相同),半自动,数据库移植不方便
案例分析:
-
基本功能
接受发送指令
根据指令自动回复对应的内容
-
模块划分
回复内容列表
回复内容维护–后台新建、更改、删除指令及对应回复的维护界面
对话功能
回复内容删除
-
项目开发流程(当然,每个人的开发顺序可能都不同)
一、实战第一步–回复内容列表模块
在没有 Mybatis的情况下,完整的Jsp + Servlet + Jdbc实现案例中的回复内容列表模块,流程如下。
1.数据库建表
2.写一个jsp前端界面
3.servlet:
-
web.xml中注册一个servlet并建立映射请求
-
写一个servlet类继承HttpServlet,重写doGet、doPost方法,用:访问数据库并把数据传给jsp。
4.代码重构下:bean装数据实体、dao层jdbc数据库增删改查、service业务操作、servlet是前后端页面控制
下面是最简单完整的Jsp + Servlet + Jdbc实现代码,工程名MicroMessage_jdbc。
bean.Message:
```java
package com.braincao.bean;
/**
- @FileName: Message
- @Author: braincao
- @Date: 2018/11/18 23:18
- @Description: 与数据库信息对应的实体类
-
一个表对应一个类、表中的一个列属性对应一个成员
*/
public class Message {
//主键
private String id;
//指令名称
private String command;
//描述
private String description;
//内容
private String content;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
dao.MessageDao:
```java
package com.braincao.dao;
import com.braincao.bean.Message;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @FileName: MessageDao
* @Author: braincao
* @Date: 2018/11/19 15:42
* @Description: 使用jdbc访问数据库,和message表相关的数据库操作
*/
public class MessageDao {
/**
* 根据查询条件查询消息列表
* @param : 查询条件:command指令名称、description描述
* @return : 根据查询条件查询到的结果
*/
public List<Message> queryListMessage(String command, String description){
List<Message> messageList = new ArrayList<>();//把查询出来的结果放在这里
try {
//连接jdbc数据库
Class.forName("com.mysql.jdbc.Driver");
//下面一定要再jdbc的数据库连接地址后面加?useUnicode=true&characterEncoding=UTF-8,否则查询不到,编码问题
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/micro_message?useUnicode=true&characterEncoding=UTF-8", "root", "123456");
//拼接查询语句
StringBuilder sql = new StringBuilder("SELECT ID, COMMAND, DESCRIPTION, CONTENT FROM message WHERE 1=1");
List<String> paramList = new ArrayList<>();
if(command!=null && !"".equals(command.trim())){//查询
sql.append(" AND COMMAND= ?");
paramList.add(command);
}
if(description!=null && !"".equals(description.trim())){//查询
sql.append(" AND DESCRIPTION LIKE '%' ? '%'");//%是sql的通配符,类似于*
paramList.add(description);
}
PreparedStatement statement = conn.prepareStatement(sql.toString());//防止sql注入
for(int i=0; i<paramList.size(); ++i){//给之前的sql语句中的 ? 赋值
statement.setString(i+1, paramList.get(i));
}
//执行sql查询语句,将查询到的结果添加到messageList列表中
ResultSet rs = statement.executeQuery();
while(rs.next()){
Message message = new Message();
message.setId(rs.getString("ID"));
message.setCommand(rs.getString("COMMAND"));
message.setDescription(rs.getString("DESCRIPTION"));
message.setContent(rs.getString("CONTENT"));
messageList.add(message);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
//返回查询结果
return messageList;
}
}
service.ListService:
package com.braincao.service;
import com.braincao.bean.Message;
import com.braincao.dao.MessageDao;
import java.util.List;
/**
* @FileName: ListService
* @Author: braincao
* @Date: 2018/11/19 15:50
* @Description: 和message表相关的业务操作
*/
public class ListService {
public List<Message> queryListMessage(String command, String description){
MessageDao messageDao = new MessageDao();
return messageDao.queryListMessage(command, description);
}
}
servlet.ListServlet:
package com.braincao.servlet;
import com.braincao.service.ListService;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @FileName: ListServlet
* @Author: braincao
* @Date: 2018/11/18 21:15
* @Description: 列表页面初始化控制的servlet,使用jdbc访问数据库
*/
@SuppressWarnings("serial")
public class ListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置编码格式
req.setCharacterEncoding("UTF-8");
//接受页面的值
String command = req.getParameter("command");
String description = req.getParameter("description");
//向页面传值
req.setAttribute("command", command);
req.setAttribute("description", description);
//查询消息列表并传给前端页面
ListService listService = new ListService();
req.setAttribute("messageList", listService.queryListMessage(command, description));
req.getRequestDispatcher("/WEB-INF/jsps/back/list.jsp").forward(req, resp);//把数据传给前端jsp
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
前端list.jsp:
<!DOCTYPE html PUBLIC "-//W3C//DTD//XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>留言评论功能</title>
<link rel="stylesheet