在学习了《Spring实战》的一些章节后,我打算写一个项目来锻炼一下。在创建项目前,我希望用全注解的方式来开发。后来就知道,debug掉的头发,都是前期拍脑袋进的水——网上关于注解的资料也太少了吧!!! 经过千辛万苦后,我终于顺利完成了项目的搭建:) 项目的源代码地址:https://github.com/FuGaZn/SpringBlog 欢迎给个star哦 ( ̄∇ ̄)
首先介绍一下这个SpringBlog项目: 本项目基于Spring+SpringMVC+Mybatis的技术框架,实现个人博客的登入登出、管理文章、在线编写和发布文章、浏览博客等基本功能。 在一步步搭建项目的过程中,你将学习到:
DispatcherServlet的配置Web界面访问的配置用Interceptor实现拦截功能自动装配(Auto wire)Controller和Service的编写基于Mybatis实现的Mapper接口以上所有功能将使用注解的形式配置! (除了使用web.xml配置对静态资源的访问)
登陆界面: 后台管理界面: 撰写文章界面: 博客主页: 查看博客界面:
新建一个Java web项目,引入Maven框架,将pom.xml的内容配置好就完成了基本的搭建。
项目结构: 本项目采用Maven来管理,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> <packaging>war</packaging> <groupId>com.fjx.blog.spring</groupId> <artifactId>SpringBlog</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.16</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20170516</version> </dependency> <!--spring依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.7.RELEASE</version> </dependency> <!--springMVC依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>2.3.1.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> <scope>compile</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> </dependency> <!--springMVC依赖结束--> </dependencies> </project>java web cofig配置类是xml配置文件的替代,在Java类上添加@Configuration注解后,该类会被Spring识别为配置类。
DispatcherServlet用于将请求发送给SpringMVC控制器(controller)DispatcherServlet 会查询一个或多个处理器映射( handler mapping ) 来确定请求的下一站在哪里。处理器映射会根据请求所携带的 URL 信息来进行决策。 MyWebInitializer.java:
package com.fjx.blog.spring.config; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class MyWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { //根配置 和我们目前的项目无关,所有不用去关心 protected Class<?>[] getRootConfigClasses() { return new Class<?>[] {RootConfig.class}; } //指定配置类 protected Class<?>[] getServletConfigClasses() { return new Class<?>[] {WebConfig.class}; } //将DispatcherServlet映射到“/”,即该DispatcherServlet会处理所有的请求 protected String[] getServletMappings() { return new String[]{"/"}; } }《Spring实战》对AbstractAnnotationConfigDispatcherServletInitializer的讲解:
InternalResourceViewResolver用于查找JSP文件,对于传入的视图名称(Controller里@RequestMapping注解的方法返回的String是视图名称),为其添加前缀和后缀,在web/webapp文件夹下查找对应的JSP文件。 例如,对以下方法:
@RequestMapping({"/index","/"}) public String index(Model model){ return "home/index"; }InternalResourceViewResolver接收到“/home/index”字符串后,拼接前缀后缀得“/WEB-INF/views/home/index.jsp”。 注意:setPrefix方法里的/WEB-INF/views/不能写成WEB-INF/views/,前者通过相对路径访问资源,后者使用绝对路径,因此后者会出错。
addResourceHandlers方法用于处理对静态资源的访问。如果不设置,访问静态资源会被拦截。 如果addResourceHandlers不起作用的话,可以在web.xml中配置如下内容:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>SpringBlog</display-name> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> <url-pattern>*.css</url-pattern> <url-pattern>*.woff</url-pattern> <url-pattern>*.svg</url-pattern> <url-pattern>*.ttf</url-pattern> <url-pattern>*.woff2</url-pattern> <url-pattern>*.eot</url-pattern> <url-pattern>*.otf</url-pattern> <url-pattern>*.ico</url-pattern> <url-pattern>*.gif</url-pattern> <url-pattern>*.jpg</url-pattern> <url-pattern>*.png</url-pattern> </servlet-mapping> </web-app>RootConfig.java:
package com.fjx.blog.spring.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @ComponentScan(basePackages = {"com.fjx.blog.spring"}, excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)) public class RootConfig { }SecurityInterceptor.java的内容:
package com.fjx.blog.spring.interceptor; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class SecurityInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws IOException { //这里根据session的用户来判断角色的权限,根据权限来转发不同的页面 if(request.getSession().getAttribute("user") == null) { response.sendRedirect("/login"); return false; } return true; } }在第一次访问被SecurityConfig拦截的界面时,因为session里的user属性不存在,所以会重定向到login界面。登陆后,添加user属性,再次访问这些界面时,就不会被重定向了。
在resources文件夹下建立properties文件,添加如下内容:
server.port=8080 # 数据库连接 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springblog # 用户名 spring.datasource.username=root # 密码 spring.datasource.password=123456 # 数据库驱动 spring.datasource.driver=com.mysql.jdbc.Driver #mybatis设置相关 mybatis.type.alias.package=com.bdqn.lyrk.ssm.study.entity logging.leve.com.fjx.blog.spring.mapper=debug mybatis.configuration.mapUnderscoreToCamelCase=true添加PropertiesConfig.java:
package com.fjx.blog.spring.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; @Configuration @PropertySource("classpath:application.properties") public class PropertiesConfig { @Value("${spring.datasource.url}") private String url; @Value("${spring.datasource.driver}") private String driver; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Value("${mybatis.type.alias.package}") private String mybatisTypeAliasPackage; public String getUrl() { return url; } public String getDriver() { return driver; } public String getUsername() { return username; } public String getPassword() { return password; } @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){ return new PropertySourcesPlaceholderConfigurer(); } }添加MyBatisConfig.java:
package com.fjx.blog.spring.config; import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @Configuration @MapperScan("com.fjx.blog.spring.mapper") public class MyBatisConfig { @Autowired PropertiesConfig propertiesConfig; @Bean //如果项目运行后报“Error querying database. Cause: java.lang.NullPointerExceptio”这样的错误,你或许要检查一下这里的配置,看看是不是url或者driverClassName为空 public DataSource dataSource(PropertiesConfig propertiesConfig) { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername(propertiesConfig.getUsername()); dataSource.setPassword(propertiesConfig.getPassword()); dataSource.setUrl(propertiesConfig.getUrl()); dataSource.setDriverClassName(propertiesConfig.getDriver()); return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); //不能用new PropertiesConfig()的方式引入参数,否则得不到变量 sessionFactory.setDataSource(dataSource(propertiesConfig)); return sessionFactory.getObject(); } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource); return dataSourceTransactionManager; } @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer(); return propertySourcesPlaceholderConfigurer; } }在以上所有文件都配置完毕后,我们就可以开发具体页面啦!是不是很激动呢,如果你是第一次开发SpringMVC项目,可能会觉得上面的配置很繁琐,但是如果你接触过用XML文件进行配置的话,那肯定会觉得上面的配置太简单了。 而且接下来,当我们配置Controller、Service、Mapper时,只需要在类上添加注解就可以完成了,不用在辛辛苦苦写完java文件后,还要在xml文件里进行添加或修改。
接下来让我们以登陆操作为例,从数据库到jsp页面,自底向上一条龙讲述其中的全部操作。
我们先在数据库中建立一张表,名字为users(在IDEA里可以图形化操作)
-- auto-generated definition CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, ukey VARCHAR(255) NULL, name VARCHAR(255) NULL, password VARCHAR(255) NULL, CONSTRAINT user_uid_uindex UNIQUE (id) ) ENGINE = InnoDB;然后创建对应的User.java:
package com.fjx.blog.spring.entity; public class User { private int id; private String name; //昵称 private String ukey; //用户身份识别key,登陆使用 private String password; //还有一系列getter和setter方法,别忘了哦。在IDEA里,同时摁住Alt和Insert键,可以一键添加全部getter和setter方法哦 }配置UserMapper.java:
package com.fjx.blog.spring.mapper; import com.fjx.blog.spring.entity.User; import org.apache.ibatis.annotations.*; import org.springframework.stereotype.Component; import java.util.List; @Mapper @Component(value = "userMapper") public interface UserMapper { @Select("select * from users") List<User> selectAll(); @Select("select * from users where ukey = #{ukey}") List<User> selectUserByKey(String ukey); @Insert("insert into users values(#{user.id},#{user.ukey},#{user.name},#{user.password},#{user.userLastLoginIp})") @Options(useGeneratedKeys = true) int save(@Param("user") User user); }@Mapper注解表明该类是一个Mybatis Mapper @Select/@Insert注解等用来操作sql语句。在传入对象参数时,在参数前使用@Param注解,否则会报 xxx not found错误。 Mybatis的注解详细用法可以参照:MyBatis常用11种注解
创建UserService和UserServiceImpl文件:
package com.fjx.blog.spring.service; import com.fjx.blog.spring.entity.User; public interface UserService { User getUserByKey(String key); void save(User user); } package com.fjx.blog.spring.service.impl; import com.fjx.blog.spring.entity.User; import com.fjx.blog.spring.mapper.UserMapper; import com.fjx.blog.spring.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User getUserByKey(String key) { List<User> users = userMapper.selectUserByKey(key); if (users == null || users.size() == 0){ return null; }else return users.get(0); } public void save(User user) { int i = userMapper.save(user); } }可以看到UserServiceImpl实现自UserServcie接口,@Service注解要放在UserServiceImpl上 使用@AutoWired注解来为UserMapper自动装配
创建LoginController.java:
package com.fjx.blog.spring.controller.admin; import com.fjx.blog.spring.entity.User; import com.fjx.blog.spring.service.UserService; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /** * @author fujiaxing * 登陆登出功能 */ @Controller public class LoginController { @Autowired UserService userService; @RequestMapping({"/login","/"}) public String login(){ return "/admin/login"; } @RequestMapping(value = "/loginVerify",method = RequestMethod.POST) @ResponseBody public String loginVerify(HttpServletRequest request, HttpServletResponse response) { Map<String, Object> map = new HashMap(); String userkey = request.getParameter("ukey"); String password = request.getParameter("password"); String rememberme = request.getParameter("rememberme"); User user = userService.getUserByKey(userkey); if (user == null) { map.put("code", 0); map.put("msg", "用户名无效!"); } else if (!user.getPassword().equals(password)) { map.put("code", 0); map.put("msg", "密码错误!"); } else { System.out.println("登陆成功"); //登录成功 map.put("code", 1); map.put("msg", ""); //添加session request.getSession().setAttribute("user", user); if (rememberme != null) { //创建两个Cookie对象 Cookie keyCookie = new Cookie("ukey", userkey); //设置Cookie的有效期为3天 keyCookie.setMaxAge(60 * 60 * 24 * 3); Cookie pwdCookie = new Cookie("password", password); pwdCookie.setMaxAge(60 * 60 * 24 * 3); response.addCookie(keyCookie); response.addCookie(pwdCookie); } // user.setUserLastLoginTime(new Date()); user.setUserLastLoginIp(getIpAddr(request)); userService.save(user); } String result = new JSONObject(map).toString(); return result; } @RequestMapping(value = "/logOut",method = RequestMethod.GET) @ResponseBody public String loginOut(HttpServletRequest request, HttpServletResponse response){ request.getSession().removeAttribute("user"); Map<String, Object> map = new HashMap(); map.put("code", 1); map.put("msg", ""); String result = new JSONObject(map).toString(); return result; } }可以看到,在LoginController里,自动装配的对象是UserService, 而不是UserServiceImpl login()方法上注解配置了映射“/login”,对应的jsp文件则是/WEB-INF/views/admin/login.jsp 上文提到,我们使用session里的user属性判断用户是否需要登陆,在执行登陆操作后,我们调用request.getSession().addAttribute()方法添加user属性。在登出系统后,我们调用request.getSession().removeAttribute()方法删除user属性
后端的所有类和方法都写好啦,接下来我们就可以编写前端界面和js代码了。 新建一个/admin/login.jsp文件,添加如下代码:
<body> <% String username = "", password = ""; Cookie[] cookies = request.getCookies(); if (cookies!=null){ for (int i = 0; i < cookies.length; i++) {//对cookies中的数据进行遍历,找到用户名、密码的数据 if ("ukey".equals(cookies[i].getName())) { username = cookies[i].getValue(); } else if ("password".equals(cookies[i].getName())) { password = cookies[i].getValue(); } } } %> <form name="loginForm" id="loginForm" method="post"> <input type="text" placeholder="用户名" name="username" id="user_login" class="input" value="<%=username%>" size="20" required/> <input type="password" placeholder="密码" name="password" id="user_pass" class="input" value="<%=password%>" size="20" required/> <p class="forgetmenot"><label for="rememberme"><input name="rememberme" type="checkbox" id="rememberme" value="1" checked /> 记住密码</label></p> <a class="submit">登陆 <input type="button" name="wp-submit" id="submit-btn" value="登录" /> </a> <p id="backtoblog" style="margin-left: 160px;color: black"><a href="/index">← 返回到博客</a></p> </form> </body> <script src="/resource/assets/js/jquery.min.js"></script> <script type="text/javascript"> $("#submit-btn").click(function () { var user = $("#user_login").val(); var password = $("#user_pass").val(); console.log(user+' '+password) if(user=="") { alert("用户名不可为空!"); } else if(password==""){ alert("密码不可为空!"); } else { $.ajax({ async: false,//同步,待请求完毕后再执行后面的代码 type: "POST", url: '/loginVerify', contentType: "application/x-www-form-urlencoded; charset=utf-8", data: $("#loginForm").serialize(), dataType: "json", success: function (data) { if(data.code==0) { alert(data.msg); } else { window.location.href="/admin"; } }, error: function () { alert("数据获取失败") } }) } })只摘取了关键代码,要看全部代码可以点击Github: SpringBlog/Login.sjp
全部完成后,运行tomcat,就可以在localhost:8080上看到我们写的界面啦,并且可以正常使用哦!
好的,本篇文章到这里就结束了。似乎写了不少内容(好吧,大部分都是代码) 注解有着比xm更简单的配置和更好的可读性,使用得当的话会大大减轻springmvc开发的复杂度和代码量。 本文也未涉及深层次的spring知识,主要还是以一个简单的登陆流程为例,讲一下如何以全注解的方式来开发一个springmvc项目。 如果对这个简单的个人博客框架有兴趣的话,可以关注https://github.com/FuGaZn/SpringBlog SpringMVC相对于SpringBoot来说,开发起来还是太复杂了。所以这个博客项目仅用于熟悉springmvc注解配置的用法,不会搭建为成熟的博客系统。
博客的在线编辑功能使用了Editormd,如果想了解Editormd的用法,可以看:Editormd的使用——在线编辑和查看文章