前言:在以前,咱们已经完成了项目的基本准备,那么就能够开始后台开发了,忽然又想到一个问题,就是准备的时候只是设计了前台的RESTful APIs,可是后台管理咱们一样也是须要API的,那么就在这一篇里面一块儿实现了吧...html
- 前序文章连接:SpringBoot技术栈搭建我的博客【项目准备】:https://www.jianshu.com/p/0293368fe750
在查了一些资料和吸取了一些评论给出良好的建议以后,我以为有必要对一些设计进行一些调整:前端
/api
开头的都是暴露出来给前端用的,凡是以/admin
开头的都是给后台使用的地址,可是意外的没有设计后天的API也把一些删除命令暴露给了前端,这就很差了从新设计设计;useActualColumnNames
使用表真正名称的东西,因此整得来生成POJO类基础字段有下划线,看着着实有点不爽,把它给干掉干掉...;把字段规范了一下,而且删除了分类下是否有效的字段(感受这种不常常变换的字段留着也没啥用干脆干掉..),因此调整为了下面这个样子(调整字段已标红):java
而后从新使用生成器自动生成对应的文件,注意记得修改generatorConfig.xml文件中对应的数据库名称;mysql
经过查资料发现其实咱们能够经过直接设置数据库来自动更新咱们的modified_by字段,而且能够像设置初始值那样给create_by和modified_by两个字段以当前时间戳设置默认值,这里具体以tbl_article_info这张表为例:android
CREATE TABLE `tbl_article_info` ( `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键', `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章标题', `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章简介,默认100个汉字之内', `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置顶,0为否,1为是', `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章访问量', `create_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间', `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
咱们经过设置DEFAULT
为CURRENT_TIMESTAMP
,而后给modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP
,这样它就会在更新的时候将该字段的值设置为更新时间,这样咱们就不用在后台关心这两个值了,也少写了一些代码(实际上是写代码的时候发现能够这样偷懒..hhh...);git
咱们须要把一些不可以暴露给前台的API收回,而后再设计一下后台的API,捣鼓了一下,最后大概是这个样子了:github
后台Restful APIs:web
前台开放RESful APIs:spring
这些API只是用来和前端交互的接口,另一些关于日志啊之类的东西就直接在后台写就好了,OK,这样就爽多了,能够开始着手写代码了;sql
随着配置内容的增多,我逐渐的想要放弃.yml的配置文件,主要的一点是这东西很差对内容进行分类(下图是简单配置了一些基本文件后的.yml和.properties文件的对比)..
最后仍是用回.properties文件吧,不分类仍是有点难受
咱们首先须要解决的是中文乱码的问题,对应GET请求,咱们能够经过修改Tomcat的配置文件【server.xml】来把它默认的编码格式改成UTF-8,而对于POST请求,咱们须要统一配置一个拦截器同样的东西把请求的编码统一改为UTF-8:
## ——————————编码设置—————————— spring.http.encoding.charset=UTF-8 spring.http.encoding.force=true spring.http.encoding.enabled=true server.tomcat.uri-encoding=UTF-8
可是这样设置以后,在后面的使用当中仍是会发生提交表单时中文乱码的问题,在网上搜索了一下找到了解决方法,新建一个【config】包建立下面这样一个配置类:
@Configuration public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter { @Bean public HttpMessageConverter<String> responseBodyConverter() { StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8")); return converter; } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { super.configureMessageConverters(converters); converters.add(responseBodyConverter()); } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorPathExtension(false); } }
决定这一次试试Druid的监控功能,因此给一下数据库的配置:
## ——————————数据库访问配置—————————— spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name = com.mysql.jdbc.Driver spring.datasource.url = jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8 spring.datasource.username = root spring.datasource.password = 123456 # 下面为链接池的补充设置,应用到上面全部数据源中 # 初始化大小,最小,最大 spring.datasource.druid.initial-size=5 spring.datasource.druid.min-idle=5 spring.datasource.druid.max-active=20 # 配置获取链接等待超时的时间 spring.datasource.druid.max-wait=60000 # 配置间隔多久才进行一次检测,检测须要关闭的空闲链接,单位是毫秒 spring.datasource.druid.time-between-eviction-runs-millis=60000 # 配置一个链接在池中最小生存的时间,单位是毫秒 spring.datasource.druid.min-evictable-idle-time-millis=300000 spring.datasource.druid.validation-query=SELECT 1 FROM DUAL spring.datasource.druid.test-while-idle=true spring.datasource.druid.test-on-borrow=false spring.datasource.druid.test-on-return=false # 打开PSCache,而且指定每一个链接上PSCache的大小 spring.datasource.druid.pool-prepared-statements=true spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20 # 配置监控统计拦截的filters,去掉后监控界面sql没法统计,'wall'用于防火墙 spring.datasource.druid.filters=stat,wall,log4j
在SpringBoot中其实已经使用了Logback来做为默认的日志框架,这是log4j做者推出的新一代日志框架,它效率更高、可以适应诸多的运行环境,同时自然支持SLF4J,在SpringBoot中咱们无需再添加额外的依赖就能使用,这是由于在spring-boot-starter-web包中已经有了该依赖了,因此咱们只须要进行配置使用就行了
当项目跑起来的时候,咱们不可能还去看控制台的输出信息吧,因此咱们须要把日志写到文件里面,在网上找到一个例子(连接:http://tengj.top/2017/04/05/springboot7/)
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="60 seconds" debug="false"> <contextName>logback</contextName> <!--本身定义一个log.path用于说明日志的输出目录--> <property name="log.path" value="/log/wmyskxz/"/> <!--输出到控制台--> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter>--> <encoder> <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!--输出到文件--> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/logback.%d{yyyy-MM-dd}.log</fileNamePattern> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="console"/> <appender-ref ref="file"/> </root> <!-- logback为java中的包 --> <logger name="cn.wmyskxz.blog.controller"/> </configuration>
在Spring Boot中你只要按照规则组织文件名,就可以使得配置文件可以被正确加载,而且官方推荐优先使用带有-spring
的文件名做为日志的配置(如上面使用的logback-spring.xml
,而不是logback.xml
),知足这样的命名规范而且保证文件在src/main/resources
下就行了;
咱们定义的目录位置为/log/wmyskxz/
,可是在项目的根目录下并无发现这样的目录,反而是在当前盘符的根目录..不是很懂这个规则..总之是成功了的..
打开是密密麻麻一堆跟控制台同样的【info】级别的信息,由于这个系统自己就比较简单,因此就没有必要去搞什么文本切割之类的东西了,ok..日志算是配置完成;
实际测试了一下,上线以后确定须要调整输出级别的,否则日志文件就会特别大...
咱们须要对地址进行拦截,对全部的/admin
开头的地址请求进行拦截,由于这是后台管理的默认访问地址开头,这是必须进行验证以后才能访问的地址,正如上面的RESTful APIs,这里包含了一些增长/删除/更改/编辑一类的操做,而通通这些操做都是不可以开放给用户的操做,因此咱们须要对这些地址进行拦截:
作验证仍是须要添加session,否则很差弄,因此咱们仍是得建立一个常规的实体:
public class User { private String username; private String password; /* getter and setter */ }
在【interceptor】包下新建一个【BackInterceptor】类并继承HandlerInterceptor接口:
public class BackInterceptor implements HandlerInterceptor { private static String username = "wmyskxz"; private static String password = "123456"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean flag = true; User user = (User) request.getSession().getAttribute("user"); if (null == user) { flag = false; } else { // 对用户帐号进行验证,是否正确 if (user.getUsername().equals(username) && user.getPassword().equals(password)) { flag = true; } else { flag = false; } } return flag; } }
在拦截器中,咱们从session中取出了user,并判断是否符合要求,这里咱们直接写死了(并无更改密码的需求,但须要加密),并且咱们并没有作任何的跳转操做,缘由很简单,根本就不须要跳转,由于访问后台的用户只有我一我的,因此只须要我知道正确的登陆地址就能够了...
刚才咱们在设置编码的时候本身建立了一个继承自WebMvcConfigurerAdapter的设置类,咱们须要复写其中的addInterceptors方法来为咱们的拦截器添加配置:
@Override public void addInterceptors(InterceptorRegistry registry) { // addPathPatterns 用于添加拦截规则 // excludePathPatterns 用户排除拦截 registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin"); super.addInterceptors(registry); }
之前咱们在写Spring MVC的时候,若是须要访问一个页面,必需要在Controller中添加一个方法跳转到相应的页面才能够,可是在SpringBoot中增长了更加方便快捷的方法:
/** * 之前要访问一个页面须要先建立个Controller控制类,在写方法跳转到页面 * 在这里配置后就不须要那么麻烦了,直接访问http://localhost:8080/toLogin就跳转到login.html页面了 * * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/admin/login").setViewName("login.html"); super.addViewControllers(registry); }
上面咱们设置了访问限制的拦截器,对后台访问进行了限制,这是拦截器的好处,咱们一样也使用拦截器对于访问数量进行一个统计
对照着数据库的设计,咱们须要保存的信息都从request对象中去获取,而后保存到数据库中便可,代码也很简单:
public class ForeInterceptor implements HandlerInterceptor { @Autowired SysService sysService; private SysLog sysLog = new SysLog(); private SysView sysView = new SysView(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 访问者的IP String ip = request.getRemoteAddr(); // 访问地址 String url = request.getRequestURL().toString(); //获得用户的浏览器名 String userbrowser = BrowserUtil.getOsAndBrowserInfo(request); // 给SysLog增长字段 sysLog.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip); sysLog.setOperateBy(StringUtils.isEmpty(userbrowser) ? "获取浏览器名失败" : userbrowser); sysLog.setOperateUrl(StringUtils.isEmpty(url) ? "获取URL失败" : url); // 增长访问量 sysView.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip); sysService.addView(sysView); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); // 保存日志信息 sysLog.setRemark(method.getName()); sysService.addLog(sysLog); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
BrowserUtil是找的网上的一段代码,直接黏贴复制放【util】包下就能够了:
/** * 用于从Request请求中获取到客户端的获取操做系统,浏览器及浏览器版本信息 * * @author:wmyskxz * @create:2018-06-21-上午 8:40 */ public class BrowserUtil { /** * 获取操做系统,浏览器及浏览器版本信息 * * @param request * @return */ public static String getOsAndBrowserInfo(HttpServletRequest request) { String browserDetails = request.getHeader("User-Agent"); String userAgent = browserDetails; String user = userAgent.toLowerCase(); String os = ""; String browser = ""; //=================OS Info======================= if (userAgent.toLowerCase().indexOf("windows") >= 0) { os = "Windows"; } else if (userAgent.toLowerCase().indexOf("mac") >= 0) { os = "Mac"; } else if (userAgent.toLowerCase().indexOf("x11") >= 0) { os = "Unix"; } else if (userAgent.toLowerCase().indexOf("android") >= 0) { os = "Android"; } else if (userAgent.toLowerCase().indexOf("iphone") >= 0) { os = "IPhone"; } else { os = "UnKnown, More-Info: " + userAgent; } //===============Browser=========================== if (user.contains("edge")) { browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-"); } else if (user.contains("msie")) { String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0]; browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1]; } else if (user.contains("safari") && user.contains("version")) { browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0] + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1]; } else if (user.contains("opr") || user.contains("opera")) { if (user.contains("opera")) { browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0] + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1]; } else if (user.contains("opr")) { browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-")) .replace("OPR", "Opera"); } } else if (user.contains("chrome")) { browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-"); } else if ((user.indexOf("mozilla/7.0") > -1) || (user.indexOf("netscape6") != -1) || (user.indexOf("mozilla/4.7") != -1) || (user.indexOf("mozilla/4.78") != -1) || (user.indexOf("mozilla/4.08") != -1) || (user.indexOf("mozilla/3") != -1)) { browser = "Netscape-?"; } else if (user.contains("firefox")) { browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-"); } else if (user.contains("rv")) { String IEVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-"); browser = "IE" + IEVersion.substring(0, IEVersion.length() - 1); } else { browser = "UnKnown, More-Info: " + userAgent; } return os + "-" + browser; } }
仍是在刚才的配置类中,新增这么一条:
@Override public void addInterceptors(InterceptorRegistry registry) { // addPathPatterns 用于添加拦截规则 // excludePathPatterns 用户排除拦截 registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin"); registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin","/admin/**"); super.addInterceptors(registry); }
在SpringBoot中,默认的错误页面比较丑(以下),因此咱们能够本身改得稍微好看一点儿,具体的教程在这里:http://tengj.top/2018/05/16/springboot13/ ,我就搞前台的时候再去弄了...
这是纠结最久应该怎么写的,一开始我还准备老老实实地利用MyBatis逆向工程生成的一堆东西去给每个实体建立一个Service的,这样其实就只是对Dao层进行了一层没必要要的封装而已,而后经过分析其实主要的业务也就分红几个:文章/评论/分类/日志浏览量这四个部分而已,因此建立这四个Service就行了;
比较神奇的事情是在网上找到一种通用Mapper的最佳实践方法,整我的都惊了,“wtf?还能够这样写哦?”,资料以下:http://tengj.top/2017/12/20/springboot11/
emmmm..咱们经过MyBatis的逆向工程,已经很大程度上简化了咱们的开发,由于在Dao层咱们已经免去了本身写SQL语句,本身写实体,本身写XML映射文件的麻烦,但在Service层咱们仍然无可避免的要写一些相似功能的代码,有没有什么方法能把这些比较通用的方法给提取出来呢? 答案就在上面的连接中,oh,简直太酷了...我决定在这里介绍一下...
在Spring4中,因为支持了泛型注解,再结合通用Mapper,咱们的想法获得了一个最佳的实践方法,下面咱们来说解一下:
咱们把一些常见的,通用的方法统一使用泛型封装在一个通用接口之中:
/** * 通用接口 * * @author: wmyskxz * @create: 2018年6月15日10:27:04 */ public interface IService<T> { T selectByKey(Object key); int save(T entity); int delete(Object key); int updateAll(T entity); int updateNotNull(T entity); List<T> selectByExample(Object example); }
/** * 通用Service * * @param <T> */ public abstract class BaseService<T> implements IService<T> { @Autowired protected Mapper<T> mapper; public Mapper<T> getMapper() { return mapper; } /** * 说明:根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号 * * @param key * @return */ @Override public T selectByKey(Object key) { return mapper.selectByPrimaryKey(key); } /** * 说明:保存一个实体,null的属性也会保存,不会使用数据库默认值 * * @param entity * @return */ @Override public int save(T entity) { return mapper.insert(entity); } /** * 说明:根据主键字段进行删除,方法参数必须包含完整的主键属性 * * @param key * @return */ @Override public int delete(Object key) { return mapper.deleteByPrimaryKey(key); } /** * 说明:根据主键更新实体所有字段,null值会被更新 * * @param entity * @return */ @Override public int updateAll(T entity) { return mapper.updateByPrimaryKey(entity); } /** * 根据主键更新属性不为null的值 * * @param entity * @return */ @Override public int updateNotNull(T entity) { return mapper.updateByPrimaryKeySelective(entity); } /** * 说明:根据Example条件进行查询 * 重点:这个查询支持经过Example类指定查询列,经过selectProperties方法指定查询列 * * @param example * @return */ @Override public List<T> selectByExample(Object example) { return mapper.selectByExample(example); } }
至此呢,咱们的通用接口就开发完成了
编写好咱们的通用接口以后,使用就变得很方便了,只须要继承相应的通用接口或者通用接口实现类,而后进行简单的封装就好了,下面以SortInfo为例:
public interface SortInfoService extends IService<SortInfo> { } ========================分割线======================== /** * 分类信息Service * * @author:wmyskxz * @create:2018-06-15-上午 11:14 */ @Service public class SortInfoServiceImpl extends BaseService<SortInfo> implements SortInfoService { }
对应到SortInfo的RESTful API设计,这样简单的继承就可以很好的支持,可是咱们仍是使用最原始的方式来建立吧...
查了一些资料,问了一下实习公司的前辈老师,而且根据咱们以前设计好的RESTful APIs,咱们颇有必要搞一个dto层用于先后端之间的数据交互,这一层主要是对数据库的数据进行一个封装整合,也方便先后端的数据交互,因此咱们首先就须要分析在dto层中应该存在哪些数据:
对应咱们的业务逻辑和RESTful APIs,我大概弄了下面几个Dto:
① ArticleDto:
该Dto封装了文章的详细信息,对应RESTful API中的/api/article/{id}
——经过文章ID获取文章信息
/** * 文章信息类 * 说明:关联了tbl_article_info/tbl_article_content/tbl_article_category/tbl_category_info/ * tbl_article_picture五张表的基础字段 * * @author:wmyskxz * @create:2018-06-19-下午 14:13 */ public class ArticleDto { // tbl_article_info基础字段 private Long id; private String title; private String summary; private Boolean isTop; private Integer traffic; // tbl_article_content基础字段 private Long articleContentId; private String content; // tbl_category_info基础字段 private Long categoryId; private String categoryName; private Byte categoryNumber; // tbl_article_category基础字段 private Long articleCategoryId; // tbl_article_picture基础字段 private Long articlePictureId; private String pictureUrl; /* getter and setter */ }
②ArticleCommentDto:
该Dto封装的事文章的评论信息,对应/api/comment/article/{id}
——经过文章ID获取某一篇文章的所有评论信息
/** * 文章评论信息 * 说明:关联了tbl_comment和tbl_article_comment两张表的信息 * * @author:wmyskxz * @create:2018-06-19-下午 14:09 */ public class ArticleCommentDto { // tbl_comment基础字段 private Long id; // 评论id private String content; // 评论内容 private String name; // 用户自定义的显示名称 private String email; private String ip; // tbl_article_comment基础字段 private Long articleCommentId; // tbl_article_comment主键 private Long articleId; // 文章ID /* getter and setter */ }
③ArticleCategoryDto:
该Dto是封装了文章的一些分类信息,对应/admin/category/{id}
——获取某一篇文章的分类信息
/** * 文章分类传输对象 * 说明:关联了tbl_article_category和tbl_category_info两张表的数据 * * @author:wmyskxz * @create:2018-06-20-上午 8:45 */ public class ArticleCategoryDto { // tbl_article_category表基础字段 private Long id; // tbl_article_category表主键 private Long categoryId; // 分类信息ID private Long articleId; // 文章ID // tbl_category_info表基础字段 private String name; // 分类信息显示名称 private Byte number; // 该分类下对应的文章数量 /* getter and setter */ }
④ArticleWithPictureDto:
该Dto封装了文章用于显示的基本信息,对应全部的获取文章集合的RESful APIs
/** * 带题图信息的文章基础信息分装类 * * @author:wmyskxz * @create:2018-06-19-下午 14:53 */ public class ArticleWithPictureDto { // tbl_article_info基础字段 private Long id; private String title; private String summary; private Boolean isTop; private Integer traffic; // tbl_article_picture基础字段 private Long articlePictureId; private String pictureUrl; /* getter and setter */ }
Service层其实就是对咱们业务的一个封装,因此有了RESTful APIs文档,咱们能够很轻易的写出对应的业务模块:
/** * 文章Service * 说明:ArticleInfo里面封装了picture/content/category等信息 */ public interface ArticleService { void addArticle(ArticleDto articleDto); void deleteArticleById(Long id); void updateArticle(ArticleDto articleDto); void updateArticleCategory(Long articleId, Long categoryId); ArticleDto getOneById(Long id); ArticlePicture getPictureByArticleId(Long id); List<ArticleWithPictureDto> listAll(); List<ArticleWithPictureDto> listByCategoryId(Long id); List<ArticleWithPictureDto> listLastest(); }
/** * 分类Service */ public interface CategoryService { void addCategory(CategoryInfo categoryInfo); void deleteCategoryById(Long id); void updateCategory(CategoryInfo categoryInfo); void updateArticleCategory(ArticleCategory articleCategory); CategoryInfo getOneById(Long id); List<CategoryInfo> listAllCategory(); ArticleCategoryDto getCategoryByArticleId(Long id); }
/** * 留言的Service */ public interface CommentService { void addComment(Comment comment); void addArticleComment(ArticleCommentDto articleCommentDto); void deleteCommentById(Long id); void deleteArticleCommentById(Long id); List<Comment> listAllComment(); List<ArticleCommentDto> listAllArticleCommentById(Long id); }
/** * 日志/访问统计等系统相关Service */ public interface SysService { void addLog(SysLog sysLog); void addView(SysView sysView); int getLogCount(); int getViewCount(); List<SysLog> listAllLog(); List<SysView> listAllView(); }
Controller层简单理解的话,就是用来获取数据的,因此只要Service层开发好了Controller层就很容易,就很少说了,只是咱们能够把一些公用的东西放到一个BaseController中,好比引入Service:
/** * 基础控制器 * * @author:wmyskxz * @create:2018-06-19-上午 11:25 */ public class BaseController { @Autowired ArticleService articleService; @Autowired CommentService commentService; @Autowired CategoryService categoryService; }
而后先后台的控制器只须要继承该类就好了,这样的方式很是值得借鉴的,只是由于这个系统比较简单,因此这个BaseController,我看过一些源码,能够在里面弄一个通用的用于返回数据的方法,好比分页数据/错误信息之类的;
按照《阿里手册》(简称)上所规范的那样,我把文章的content单独弄成了一张表而且将这个“可能很长”的字段的类型设置成了text类型,可是MyBatis逆向工程自动生成的时候,却把这个text类型的字段单独给列了出去,即在生成的xml中多出了一个<resultMap>
,标识id为ResultMapWithBLOBs
,MyBatis这样作可能的缘由仍是怕这个字段太长影响前面的字段查询吧,可是操做这样的LONGVARCHAR
类型的字段MyBatis好像并无集成很好,因此想要很好的操做仍是须要给它弄成VARCHAR类型才行;
在generatorConfig.xml中配置生成字段的时候加上这样一句话就行了:
<table domainObjectName="ArticleContent" tableName="tbl_article_content"> <columnOverride column="content" javaType="java.lang.String" jdbcType="VARCHAR" /> </table>
在编写前台拦截器的时候,我使用@Autowired
注解自动注入了SysService系统服务Service,可是却报nullpointer的错,发现是没有自动注入上,SysService为空..这是为何呢?排除掉注解没有识别或者没有给Service添加上注解的可能性以后,我发现好像是拦截器拦截的时候Service并无建立成功形成的,参考这篇文章:https://blog.csdn.net/slgxmh/article/details/51860278,成功解决问题:
@Bean public HandlerInterceptor getForeInterceptor() { return new ForeInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { // addPathPatterns 用于添加拦截规则 // excludePathPatterns 用户排除拦截 registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin"); registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin", "/admin/**"); super.addInterceptors(registry); }
其实就是添加上@Bean
注解让ForeInterceptor提早加载;
当时设计表的时候,就只是单纯的想要保存一下用户使用的浏览器是什么,其实当时并不知道应该怎么获取获取到的东西又是什么,只是以为保存浏览器20个字段够了,但后来发现这是很蠢萌的...因此不得不调整数据库的字段长度,好在只须要单方面调整数据库的字段长度就行了:
由于我想要在数据库中保存的是md源码,而返回前台前端但愿的是直接拿到html代码,这样就能很方便的输出了,因此这要怎么作呢?找到一篇参考文章:https://my.oschina.net/u/566591/blog/1535380
咱们不要搞那么复杂的封装,只要简单弄一个工具类就能够了,在【util】包下新建一个【Markdown2HtmlUtil】:
/** * Markdown转Html工具类 * * @author:wmyskxz * @create:2018-06-21-上午 10:09 */ public class Markdown2HtmlUtil { /** * 将markdown源码转换成html返回 * * @param markdown md源码 * @return html代码 */ public static String markdown2html(String markdown) { MutableDataSet options = new MutableDataSet(); options.setFrom(ParserEmulationProfile.MARKDOWN); options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[]{TablesExtension.create()})); Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); Node document = parser.parse(markdown); return renderer.render(document); } }
使用也很简单,只须要在获取一篇文章的时候把ArticleDto里面的md源码转成html代码再返回给前台就行了:
/** * 经过文章的ID获取对应的文章信息 * * @param id * @return 本身封装好的文章信息类 */ @ApiOperation("经过文章ID获取文章信息") @GetMapping("article/{id}") public ArticleDto getArticleById(@PathVariable Long id) { ArticleDto articleDto = articleService.getOneById(id); articleDto.setContent(Markdown2HtmlUtil.markdown2html(articleDto.getContent())); return articleDto; }
样式之类的交给前台就行了,搞定...
关于统计啊日志类的Controller尚未开发,RESful API也没有设计,这里就先发布文章了,由于好像时间有点紧,后台的页面暂时可能开发不完,准备直接开始前台页面显示的开发(主要是本身对前端不熟悉还要学习..),这里对后台进行一个简单的总结:
其实发现当数据库设计好了,RESful APIs设计好了以后,后台的任务变得很是明确,开发起来也就思路很清晰了,只是本身仍是缺乏一些必要的经验,如对一些通用方法的抽象/层与层之间数据交互的典型设计之类的东西,特别是一些安全方面的东西,网上的资料也比较少一些,也是本身须要学习的地方;
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz 欢迎关注公众微信号:wmyskxz_javaweb 分享本身的Java Web学习之路以及各类Java学习资料