SpringBoot技术栈搭建我的博客【后台开发】

前言:在以前,咱们已经完成了项目的基本准备,那么就能够开始后台开发了,忽然又想到一个问题,就是准备的时候只是设计了前台的RESTful APIs,可是后台管理咱们一样也是须要API的,那么就在这一篇里面一块儿实现了吧...html

一些设计上的调整

在查了一些资料和吸取了一些评论给出良好的建议以后,我以为有必要对一些设计进行一些调整:前端

  • 1)数据库:命名应该更加规范,好比表示分类最好用category而不是sort,表示评论最好用comment而不是message;
  • 2)RESful APIs:在准备着手开始写后台的时候就已经发现,原本想的是凡是以/api开头的都是暴露出来给前端用的,凡是以/admin开头的都是给后台使用的地址,可是意外的没有设计后天的API也把一些删除命令暴露给了前端,这就很差了从新设计设计;
  • 3)命名规范的问题:由于使用MyBatis逆向工程自动生成的时候,配置了一个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;

咱们经过设置DEFAULTCURRENT_TIMESTAMP,而后给modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP,这样它就会在更新的时候将该字段的值设置为更新时间,这样咱们就不用在后台关心这两个值了,也少写了一些代码(实际上是写代码的时候发现能够这样偷懒..hhh...);git

RESTful APIs从新设计

咱们须要把一些不可以暴露给前台的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包中已经有了该依赖了,因此咱们只须要进行配置使用就行了

第一步:建立logback-spring.xml

当项目跑起来的时候,咱们不可能还去看控制台的输出信息吧,因此咱们须要把日志写到文件里面,在网上找到一个例子(连接: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,这里包含了一些增长/删除/更改/编辑一类的操做,而通通这些操做都是不可以开放给用户的操做,因此咱们须要对这些地址进行拦截:

第一步:建立User实体类

作验证仍是须要添加session,否则很差弄,因此咱们仍是得建立一个常规的实体:

public class User {
    private String username;
    private String password;

    /* getter and setter */
}

第二步:建立拦截器并继承HandlerInterceptor接口

在【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,并判断是否符合要求,这里咱们直接写死了(并无更改密码的需求,但须要加密),并且咱们并没有作任何的跳转操做,缘由很简单,根本就不须要跳转,由于访问后台的用户只有我一我的,因此只须要我知道正确的登陆地址就能够了...

第三步:在配置类中复写addInterceptors方法

刚才咱们在设置编码的时候本身建立了一个继承自WebMvcConfigurerAdapter的设置类,咱们须要复写其中的addInterceptors方法来为咱们的拦截器添加配置:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    super.addInterceptors(registry);
}
  • 说明:这个方法也很简单,经过在addPathPatterns中添加拦截规则(这里设置拦截/admin开头的全部地址),并经过excludePathPatterns来排除拦截的地址(这里为/toLogin,即登陆地址,到时候我能够弄得复杂隐蔽一点儿)

第四步:配置登陆页面

之前咱们在写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);
}
  • 注意:login.html记得要放在【templates】下才会生效哦...(我试过使用login绑定视图名不成功,只能写全了...)

访问日志记录

上面咱们设置了访问限制的拦截器,对后台访问进行了限制,这是拦截器的好处,咱们一样也使用拦截器对于访问数量进行一个统计

第一步:编写前台访问拦截器

对照着数据库的设计,咱们须要保存的信息都从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 {

    }
}
  • 注意:可是须要注意的是测试的时候别把拦截器开了(主要是postHandle方法中中没法强转handler),否则不方便测试...

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/ ,我就搞前台的时候再去弄了...


Service 层开发

这是纠结最久应该怎么写的,一开始我还准备老老实实地利用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设计,这样简单的继承就可以很好的支持,可是咱们仍是使用最原始的方式来建立吧...

Service接口申明

查了一些资料,问了一下实习公司的前辈老师,而且根据咱们以前设计好的RESTful APIs,咱们颇有必要搞一个dto层用于先后端之间的数据交互,这一层主要是对数据库的数据进行一个封装整合,也方便先后端的数据交互,因此咱们首先就须要分析在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接口开发

Service层其实就是对咱们业务的一个封装,因此有了RESTful APIs文档,咱们能够很轻易的写出对应的业务模块:

文章Service

/**
 * 文章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

/**
 * 分类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

/**
 * 留言的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

/**
 * 日志/访问统计等系统相关Service
 */
public interface SysService {
    void addLog(SysLog sysLog);

    void addView(SysView sysView);

    int getLogCount();

    int getViewCount();

    List<SysLog> listAllLog();

    List<SysView> listAllView();
}

Controller 层开发

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,我看过一些源码,能够在里面弄一个通用的用于返回数据的方法,好比分页数据/错误信息之类的;


记录坑

1)MyBatis中Text类型的坑

按照《阿里手册》(简称)上所规范的那样,我把文章的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>

2)拦截器中Service注入为null的坑

在编写前台拦截器的时候,我使用@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提早加载;

3)数据库sys_log表中operate_by字段的坑

当时设计表的时候,就只是单纯的想要保存一下用户使用的浏览器是什么,其实当时并不知道应该怎么获取获取到的东西又是什么,只是以为保存浏览器20个字段够了,但后来发现这是很蠢萌的...因此不得不调整数据库的字段长度,好在只须要单方面调整数据库的字段长度就行了:

4)保存文章的方式的坑

由于我想要在数据库中保存的是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学习资料

相关文章
相关标签/搜索