端午节前一周重新研究了一波当前开发系统的架构,参考诸多文档资料写了一个springboot+veloctiy+mybatis+shiro的demo,并将学习过程记录在此。后续若有时间,将会对此demo丰富升级,将后续学习的东西都填充入本demo。
https://github.com/2500284064/springboot-demo
按照时间顺序记录demo开发过程:
springboot+velocity集成
目录格式:
--springdemo
--src
--main
--java
--com.example
--config //存放配置类
--controller //控制器
--db //数据库entity 和mapper接口
--service //服务层
--Starter.java //启动类
--resources
--assets //静态资源
--mapper //mybatis mapper.xml目录
--templates //veloctiy模板
--application.yml
--pom.xml
文档目录层次如上述格式所示,其中pom.xml和src同在根目录之下,src目录至java目录和 resources目录是spring代码已经写死的目录结构。而java目录下的目录结构可随意构建,不过个人习惯将项目的groupId作为java下的包目录名。建议将启动类放在最外层包的根目录下,原因是spring默认将启动类所属的包作为扫描的目录,当然也可以自己配置扫描包目录。
此外,采用velocity模板语言时,默认的vm文档根目录为resources/templates。此目录可在application.yml中配置。(application.yml是项目的配置文档,默认在resources根目录下)
ps:后续将com.example目录称之为java根目录,resources称之为资源根目录
2、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>
<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!--spring-boot-starter-parent 采取保守策略,可以修改为最新的jdk-->
<properties>
<java.version>1.8</java.version>
</properties>
<!--可提供诸多默认配置,支持属性覆盖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.3.RELEASE</version>
</parent>
<!-- Additional lines to be added here... -->
<dependencies>
<!--提供web支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--velocity支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-velocity</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!--编译插件,打包成可执行jar-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
<version>1.4.0.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
springboot+velocity的项目搭建只需要上述的依赖。依赖的作用已经在注解中说明。
3、配置application.yml
server:
port: 8001 #端口号
session:
timeout: 6000 #second
spring:
aop:
auto: true
proxy-target-class: true
velocity:
cache: false
charset: UTF-8
expose-spring-macro-helpers: true
properties: #配置宏所在文档,自行google
velocimacro.library: macro/macro.vm
velocimacro.library.autoreload: true
resource-loader-path: classpath:/templates/ #vm文档扫描目录
suffix: .vm
4、配置启动类和velocityConfig配置类
之前已经说过,建议启动类在java根目录下,启动类的写法如无特殊需求其实很固定:
@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = "com.example") //此时由于主类在根目录下,所以默认的扫描目录即为com.example目录
//上面三个注解可用@SpringBootApplication代替
//@MapperScan(value = "com.example.db.mapper") 此注解被MapperScannerConfig类替代
@EnableTransactionManagement(proxyTargetClass = true) //支持事务管理
public class ApiStarter {
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(ApiStarter.class);
//此处可对app进行自定义设置
app.run(args);
}
}
velocityConfig配置类用于配置VelocityLayoutViewResolver类解析velocity模板。同时可配置默认布局,
所谓布局,是指项目中后台返回的页面将会作为html嵌入设置的布局页面中引用$screen_content的位置,采用布局的好处的可以统一诸如外部js,css和页面头部尾部等公用的部分。此外布局可在后台返回时自主设置。
@Configuration
public class VelocityConfig {
/*name property is chooseable*/
/*配置布局,默认布局文档夹为layout, 且若不使用VelocityProperties 参数则无法使用布局*/
@Bean(name = "velocityResolver")
public VelocityLayoutViewResolver velocityLayoutViewResolver(VelocityProperties properties){
VelocityLayoutViewResolver resolver = new VelocityLayoutViewResolver();
properties.applyToViewResolver(resolver);
resolver.setLayoutUrl("layout/default.vm");
return resolver;
}
}
5、撰写基本Controller和login页面(login页面后面集成shiro用作测试录)
/*IndexController:*/
@Controller
public class IndexController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(){
return "login";
}
}
login.vm: 登录页面,后续用作shiro的登录测试页面
<script>
$(function () {
$("#signIn").on("click", function () {
$("#loginForm").submit();
})
})
</script>
<div class="page-content" style="background-image: url('/assets/images/earth-banner.jpg'); background-repeat: no-repeat; height: 415px">
<div class="login-panel">
<form id="loginForm" action="/login" method="post">
<div class="errorMsg">$!{errorMsg}</div>
<input type="text" name="userName" value="" placeholder="账号"/>
<input type="password" name="password" value="" placeholder="密码"/>
<input type="button" class="btn btn-primary btn-sm" id="signIn" value="登录"/>
</form>
</div>
</div>
至此就可以运行测试了:启动项目后,在浏览器访问:localhost:8001/login, 若返回登录页面则成功。
集成mybatis
ORM层目前使用的较多的技术有hibernate何mybatis两种,mybatis相对来说更加灵活,这里指的灵活,很大一部分在于mybatis的SQL语言需要手写高级SQL语句,手写SQL语句的好处在于可以根据需要进行优化,诸如仅仅查询特定的字段等。当然这也表明了mybatis相对更加简陋。
1、配置pom.xml
本项目使用的是oracle数据库,因此引入oracle依赖。
<!--oracle-->
<!--连接oracle必要依赖-->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc7</artifactId>
<version>12.1.0.1</version>
</dependency>
<!--mybatis start-->
<!--整合mybatis必要依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!--mybatis end-->
2、配置application.yml
配置数据源,在spring下配置如下datasource(也可以在mybatis的配置文档中配置):
datasource:
url: jdbc:oracle:thin:@localhost:1521:orcl
username: username
password: password
driver-class-name: oracle.jdbc.driver.OracleDriver
配置mybatis配置,包括管理sql的xml文档目录,entity目录,和配置文档目录:
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.db.entity
config-location: classpath:mybatis-config.xml
此处,type-aliases-package 属性可以不配置,mapper-locations属性若不配置的话,则默认的sql.xml扫描目录即为mapperScanner的扫描路径。
config-location指明mybatis配置文档在resources根目录下。
3、配置mapperScanner
创建config文档 MybatisMapperScanConfig:
/**
*
* 配置mapper接口的目录,若application配置文档中未配置mybatis.mapper-location属性,
* 则默认在相同目录(即可在resources目录下,也可java目录下)下扫描查找mapper.xml文档
*/
@Configuration
public class MybatisMapperScanConfig {
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScanner = new MapperScannerConfigurer();
mapperScanner.setBasePackage("com.example.db.mapper");
return mapperScanner;
}
}
此Java类配置非必需,最简单的方法是在启动类添加注解 @MapperScan(value = “com.example.db.mapper”)
4、自动生成数据库实体类、mapper接口和mapper.xml
通过上面3步骤,mybatis的配置基本完成,mybatis 提供了 generator自动生成需要获取的数据库实体类、mapper接口和mapper.xml,只需要在pom中引入依赖再配置生成规则即可。
pom中配置插件:
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<!--此处声明了生成规则配置文档的路径-->
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin>
在pom引入的generator插件中配置好生成规则文档路径名称,并创建该配置文档:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration >
<properties resource="generatorConfig.properties"/>
<!-- 指定数据连接驱动jar地址 -->
<classPathEntry location="${classPath}" />
<!-- 一个数据库一个context -->
<context id="pis-dev" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="false"/><!-- 是否取消注释 -->
<property name="suppressDate" value="true" /> <!-- 是否生成注释代时间戳-->
</commentGenerator>
<jdbcConnection driverClass="${jdbc.driver}"
connectionURL="${jdbc.url}" userId="${jdbc.username}" password="${jdbc.password}" >
</jdbcConnection>
< 大专栏 springbootdemo;!-- 类型转换 -->
<javaTypeResolver>
<!-- 是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.) -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<javaModelGenerator targetPackage="com.example.db.entity" targetProject="${db.project}/src/main/java" >
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject="${db.project}/src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<javaClientGenerator targetPackage="com.example.db.mapper" targetProject="${db.project}/src/main/java" type="XMLMAPPER" >
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!-- schema即为数据库名 tableName为对应的数据库表 domainObjectName是要生成的实体类 enable*ByExample 是否生成 example类 -->
<!-- 忽略列,不生成bean 字段 -->
<!-- <ignoreColumn column="FRED" /> -->
<!-- 指定列的java数据类型 -->
<!-- <columnOverride column="LONG_VARCHAR_FIELD" jdbcType="VARCHAR" /> -->
<table tableName="sys_right" domainObjectName="Right" enableCountByExample="false"
enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false" >
</table>
</context>
</generatorConfiguration>
配置很好理解,不赘述,本项目中将配置文档中需要用到的参数提取出来,其实可直接写在配置文档中,不过不推荐。
配置完毕后,即可运行mvn clean package命令,mybatis将会自动根据配置生成实体类等。
pS:生成规则的配置中,生成实体类,mapper.xml等文档的目录应当与mapperScanner和mapper-locations对应。且生成完毕后,需要将pom中的插件注释,否则每次运行项目均会再次生成
Shiro集成
本测试项目之所以选择Shiro作为权限控制,主要原因是实际工作项目用到的即为Shiro,所以想借着此demo深入了解学习shiro的使用。
spring其实自带了spring-security作安全管理,这两者实现的功能基本一致,相对来说shiro更为简单一些,好用易学,也是java官方所推荐的。
本demo配置实现方法:
1、引入依赖
pom中引入依赖的jar.其中shiro-core是所有使用shiro必须的,shiro-spring提供对spring框架的支持。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
2、实现Realm
Realm是shiro中唯一需要自己实现的类,用来实现具体的登录验证,权限赋予的功能。
Realm继承自抽象类AutorizingRealm, AutorizingRealm中已经实现了大多数代码,具体需要开发人员编写的即为上面所说的登录验证(重写doGetAuthenticationInfo方法),权限赋予(重写doGetAuthorizationInfo方法)。
eg:
/*登录认证*/
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken aToken){
//根据用户输入的用户名获取数据库中的用户信息。
UsernamePasswordToken token = (UsernamePasswordToken) aToken;
User user = userService.selectUserByUserName(token.getUsername());
if(user != null){
//将查询到的用户和密码存放到 authenticationInfo用于后面的权限判断
//(第二个参数为数据库中的密码,将用来与输入的密码匹配,
//第一个参数为存入的登录用户信息,之后可以在其他地方获取当前登录用户信息)。
//第三个参数随便。
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), "realmName") ;
return authenticationInfo ;
}
return null;
}
/*权限认证,执行完登录认证后将进行权限赋予*/
@Override
public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal){
//此处入参principal即为上一方法返回值的第一个参数:登录用户User
User user = (User) principal.getPrimaryPrincipal();
logger.debug("userName = " + user.getUserName());
if(StringUtils.isEmpty(user.getId())) return null;
/*添加角色和权限*/
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Role> roles = userService.selectRolesByUserId(user.getId());
List<Right> rights = userService.selectRightsByUserId(user.getId());
info.setRoles(roles.stream().map( a -> a.getRoleName()).collect(Collectors.toSet()));
info.setStringPermissions(rights.stream().map( a -> a.getPermission()).collect(Collectors.toSet()));
return info;
}
3、ShiroConfig
自己实现了登录认证和权限赋予之后,还需要对Shiro进行其他配置,如ShiroFilterFactoryBean和SecurityManager,这两者是必须配置的。
ShiroFilterFactoryBean用来处理资源拦截问题,ShiroFilterFactoryBean中必须注入SecurityManager,这是ShiroFilter的核心安全接口。
下面直接贴出demo的配置,(主要了解ShiroFilterFactoryBean的配置即可,PS:ShiroFilterFactoryBean依赖于SecurityManager)
@Configuration
public class ShiroConfig {
private static Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
/**
* ShiroFilterFactoryBean 处理拦截资源文档问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/login"); /*要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面*/
shiroFilter.setSuccessUrl("/index"); /*登录成功页面*/
shiroFilter.setUnauthorizedUrl("/forbidden"); /*无权限转页面*/
/*定义shiro过滤链 Map结构 * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的*/
Map<String, String> filterChainDefinitionMapping = new HashMap<String, String>();
/*anon 静态资源,匿名访问*/
filterChainDefinitionMapping.put("/favicon.ico", "anon");
filterChainDefinitionMapping.put("/forbidden", "anon");
filterChainDefinitionMapping.put("/assets/**", "anon");
filterChainDefinitionMapping.put("/webjars/**", "anon");
filterChainDefinitionMapping.put("/v2/api-docs", "anon");
filterChainDefinitionMapping.put("/login", "authc"); //需要认证
filterChainDefinitionMapping.put("/logout", "logout"); //退出登录,返回登录页面
filterChainDefinitionMapping.put("/**", "user"); //判断登陆状态
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMapping);
Map<String, Filter> filters = new HashMap<String, Filter>();
filters.put("anon", new AnonymousFilter());
filters.put("authc", new FormAuthenticationFilter());
filters.put("logout", new LogoutFilter());
filters.put("user", new UserFilter());
shiroFilter.setFilters(filters);
return shiroFilter;
}
@Bean
public RememberMeManager rememberMeManager() {
logger.debug("create remember me manager.");
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(2592000);
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
rememberMeManager.setCipherKey(org.apache.shiro.codec.Base64.decode("Pis2016%KyEe^!#/"));
rememberMeManager.setCookie(simpleCookie);
return rememberMeManager;
}
/*指定名字,防止与spring cache 冲突*/
@Bean(name = "shiroCacheManager")
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//注入缓存管理器;
securityManager.setCacheManager(cacheManager());
securityManager.setRealm(realm());
securityManager.setRememberMeManager(rememberMeManager());
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}
@Bean(name = "realm")
@DependsOn("lifecycleBeanPostProcessor")
public ShiroRealm realm() {
ShiroRealm realm = new ShiroRealm();
// Md5加密
// realm.setCredentialsMatcher(new HashedCredentialsMatcher(Md5Hash.ALGORITHM_NAME));
return realm;
}
/*shiro生命周期处理器*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/*开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
@ConditionalOnMissingBean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(securityManager);
return aasa;
}
@Bean
/*设置基于表单的身份验证过滤器*/
public FormAuthenticationFilter formAuthenticationFilter(){
FormAuthenticationFilter filter = new FormAuthenticationFilter();
filter.setFailureKeyAttribute("shiroLoginFailure");
filter.setUsernameParam("userName");
filter.setPasswordParam("password");
filter.setRememberMeParam("rememberMe");
return filter;
}
}
4、登录
配置完成后,只需要访问ShiroFilter配置的LoginUrl(默认为login.jsp),就会自动获取传入的参数username和password( 必须全小写,如需要自定义,可配置基于表单的身份验证过滤器FormAuthenticationFilter)封装为UsernamePasswordToken,传递个Realm的登录验证方法处理。
而实际的login方法中只需要获取错误信息分别处理即可。
//login的实现不需要自己实现,在ShiroConfig中的Shiro过滤器声明登录URL,则会在此url被请求时,
//自动获取 username 和 password 参数,作为登录账号和密码
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String signInBak(HttpServletRequest request, ModelMap model){
String errorClassName = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
logger.info("errorClassName {}", errorClassName);
if(errorClassName != null) {
if (errorClassName.endsWith("UnknownAccountException") ||
errorClassName.endsWith("IncorrectCredentialsException")) {
model.addAttribute("errorMsg", "账号或密码错误");
} else {
model.addAttribute("errorMsg", "未知错误,请联系管理员.");
}
}
return "/login";
}
如果不需要访问login自动shiro验证,可以手动登录,只需要SecurityUtils.getSubject()获取到subject,然后调用subject.login(token)方法, 传入UsernamePasswordToken作为参数即可手动登录验证。
总结
至此,demo框架已经完成,这篇随笔基本记录了实现的过程,贴入了很多代码,也比较混乱,暂且先记着,加深学习印象。后续将会另起一到两篇记录日志管理和缓存管理的添加,待续。