springboot 可以说是现在做javaweb开发最火的技术,我在基于springboot搭建项目的过程中,踩过不少坑,发现整合框架时并非仅仅引入starter 那么简单。

要做到简单,易用,扩展性更好,还需做不少二次封装,于是便写了个基于springboot的web项目脚手架,对一些常用的框架进行整合,并进行了简单的二次封装。

项目名baymax取自动画片超能陆战队里面的大白,大白是一个医护充气机器人,希望这个项目你能像大白一样贴心,可以减少你的工作量。

github https://github.com/zhaoguhong/baymax

web

web模块是开发web项目必不可少的一个模块

maven 依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

对于前后端分离项目,推荐直接使用@RestController注解

需要注意的是,不建议直接用RequstMapping注解并且不指定方法类型的写法,推荐使用GetMaping或者PostMaping之类的注解

@SpringBootApplication
@RestController
public class BaymaxApplication { public static void main(String[] args) {
SpringApplication.run(BaymaxApplication.class, args);
} @GetMapping("/test")
public String test() {
return "hello baymax";
}
}

单元测试

spring 对单元测试也提供了很好的支持

maven 依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

添加 @RunWith(SpringRunner.class)@SpringBootTest 即可进行测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {
}

对于Controller层的接口,可以直接用MockMvc进行测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest { @Autowired
private WebApplicationContext context;
private MockMvc mvc; @Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
} @Test
public void testValidation() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/test"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.content().string("hello baymax"));
} }

actuator应用监控

actuator 是 spring 提供的应用监控功能,常用的配置项如下

# actuator端口 默认应用端口
management.server.port=8082
# 加载所有的端点 默认只加载 info,health
management.endpoints.web.exposure.include=*
# actuator路径前缀,默认 /actuator
management.endpoints.web.base-path=/actuator

lombok

lombok可以在编译期生成对应的java代码,使代码看起来更简洁,同时减少开发工作量

用lombok后的实体类

@Data
public class Demo {
private Long id;
private String userName;
private Integer age;
}

需要注意,@Data 包含 @ToString、@Getter、@Setter、@EqualsAndHashCode、@RequiredArgsConstructorRequiredArgsConstructor 并不是无参构造,无参构造的注解是NoArgsConstructor

RequiredArgsConstructor 会生成 会生成一个包含常量(final),和标识了@NotNull的变量 的构造方法

baseEntity

把表中的基础字段抽离出来一个BaseEntity,所有的实体类都继承该类

/**
* 实体类基础类
*/
@Data
public abstract class BaseEntity implements Serializable {
/**
* 主键id
*/
private Long id;
/**
* 创建人
*/
private Long createdBy;
/**
* 创建时间
*/
private Date createdTime;
/**
* 更新人
*/
private Long updatedBy;
/**
* 更新时间
*/
private Date updatedTime;
/**
* 是否删除
*/
private Integer isDeleted; }

统一响应返回值

前后端分离项目基本上都是ajax调用,所以封装一个统一的返回对象有利于前端统一处理

/**
* 用于 ajax 请求的响应工具类
*/
@Data
public class ResponseResult<T> {
// 未登录
public static final String UN_LOGIN_CODE = "401";
// 操作失败
public static final String ERROR_CODE = "400";
// 服务器内部执行错误
public static final String UNKNOWN_ERROR_CODE = "500";
// 操作成功
public static final String SUCCESS_CODE = "200";
// 响应信息
private String msg;
// 响应code
private String code;
// 操作成功,响应数据
private T data; public ResponseResult(String code, String msg, T data) {
this.msg = msg;
this.code = code;
this.data = data;
}
}

返回给前端的值用ResponseResult包装一下

  /**
* 测试成功的 ResponseResult
*/
@GetMapping("/successResult")
public ResponseResult<List<Demo>> test() {
List<Demo> demos = demoMapper.getDemos();
return ResponseResult.success(demos);
} /**
* 测试失败的 ResponseResult
*/
@GetMapping("/errorResult")
public ResponseResult<List<Demo>> demo() {
return ResponseResult.error("操作失败");
}

ResponseEntity

spring其实封装了ResponseEntity 处理响应,ResponseEntity 包含 状态码,头部信息,响应体 三部分

  /**
* 测试请求成功
* @return
*/
@GetMapping("/responseEntity")
public ResponseEntity<String> responseEntity() {
return ResponseEntity.ok("请求成功");
} /**
* 测试服务器内部错误
* @return
*/
@GetMapping("/InternalServerError")
public ResponseEntity<String> responseEntityerror() {
return new ResponseEntity<>("出错了", HttpStatus.INTERNAL_SERVER_ERROR);
}

异常

自定义异常体系

为了方便异常处理,定义一套异常体系,BaymaxException 做为所有自定义异常的父类

// 项目所有自定义异常的父类
public class BaymaxException extends RuntimeException
// 业务异常 该异常的信息会返回给用户
public class BusinessException extends BaymaxException
// 用户未登录异常
public class NoneLoginException extends BaymaxException

全局异常处理

对所有的异常处理后再返回给前端

@RestControllerAdvice
public class GlobalControllerExceptionHandler { /**
* 业务异常
*/
@ExceptionHandler(value = {BusinessException.class})
public ResponseResult<?> handleBusinessException(BusinessException ex) {
String msg = ex.getMessage();
if (StringUtils.isBlank(msg)) {
msg = "操作失败";
}
return ResponseResult.error(msg);
} /**
* 处理未登录异常
*/
@ExceptionHandler(value = {NoneLoginException.class})
public ResponseResult<?> handleNoneLoginException(NoneLoginException ex) {
return ResponseResult.unLogin();
}

异常持久化

对于未知的异常,保存到数据库,方便后续排错

需要说明是的,如果项目访问量比较大,推荐用 ELK 这种成熟的日志分析系统,不推荐日志保存到关系型数据库

  @Autowired
private ExceptionLogMapper exceptionLogMapper;
/**
* 处理未知的错误
*/
@ExceptionHandler(value = {Exception.class})
public ResponseResult<Long> handleunknownException(Exception ex) {
ExceptionLog log = new ExceptionLog(new Date(), ExceptionUtils.getStackTrace(ex));
exceptionLogMapper.insert(log);
ResponseResult<Long> result = ResponseResult.unknownError("服务器异常:" + log.getId());
result.setData(log.getId());
return result;
}

异常日志接口

对外开一个异常日志查询接口/anon/exception/{异常日志id},方便查询

@RestController
@RequestMapping("/anon/exception")
public class ExceptionController { @Autowired
private ExceptionLogMapper exceptionLogMapper; @GetMapping(value = "/{id}")
public String getDemo(@PathVariable(value = "id") Long id) {
return exceptionLogMapper.selectByPrimaryKey(id).getException();
} }

数据校验

JSR 303定义了一系列的 Bean Validation 规范,Hibernate Validator 是 Bean Validation 的实现,并进行了扩展

spring boot 使用 也非常方便

public class Demo extends BaseEntity{
@NotBlank(message = "用户名不允许为空")
private String userName;
@NotBlank
private String title;
@NotNull
private Integer age;
}

参数前面添加@Valid注解即可

  @PostMapping("/add")
public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
demoMapper.insert(demo);
return ResponseResult.success();
}

对于校验结果可以每个方法单独处理,如果不处理,会抛出有异常,可以对校验的异常做全局处理

GlobalControllerExceptionHandler 添加

  /**
* 处理校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseResult<?> handleValidationException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
if (result.hasErrors()) {
StringJoiner joiner = new StringJoiner(",");
List<ObjectError> errors = result.getAllErrors();
errors.forEach(error -> {
FieldError fieldError = (FieldError) error;
joiner.add(fieldError.getField() + " " + error.getDefaultMessage());
});
return ResponseResult.error(joiner.toString());
} else {
return ResponseResult.error("操作失败");
}
}

log

spring boot 的默认使用的日志是logback,web模块依赖的有日志 starter,所以这里不用再引入依赖,详细配置

修改日志级别

Actuator 组件提供了日志相关接口,可以查询日志级别或者动态修改日志级别

// 查看所有包/类的日志级别
/actuator/loggers
// 查看指定包/类日志级别 get 请求
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController
//修改日志级别 post 请求 参数 {"configuredLevel":"debug"}
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController

日志切面

添加一个日志切面,方便记录方法执行的入参和出参

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAspect { /**
* 日志描述
*/
String value() default ""; /**
* 日志级别
*/
String level() default "INFO"; }

使用时直接添加到方法上即可

swagger

swagger 是一个很好用的文档生成工具

maven 依赖

    <dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>

相关配置

@Configuration
@EnableSwagger2
public class SwaggerConfig { @Value("${swagger.enable:false}")
private boolean swaggerEnable; //文档访问前缀
public static final String ACCESS_PREFIX = "/swagger-resources/**,/swagger-ui.html**,/webjars/**,/v2/**"; @Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
// 设置是否开启swagger,生产环境关闭
.enable(swaggerEnable)
.select()
// 当前包路径
.apis(RequestHandlerSelectors.basePackage("com.zhaoguhong.baymax"))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
} // 构建api文档的详细信息
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 页面标题
.title("接口文档")
// 创建人
.contact(new Contact("孤鸿", "https://github.com/zhaoguhong/baymax", ""))
// 版本号
.version("1.0")
// 描述
.description("大白的接口文档")
.build();
}
}

为文档加一个开关

#是否开启swagger文档,生产环境关闭
swagger.enable=true

然后就可以愉快的在写代码的同时写文档了

  @PostMapping("/add")
@ApiOperation(value = "新增 demo")
public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
demoMapper.insert(demo);
return ResponseResult.success();
}
@ApiModel("示例")
public class Demo extends BaseEntity{
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("标题")
private String title;
@ApiModelProperty("年龄")
private Integer age;
}

访问 localhost:8080/swagger-ui.html 就可以看到效果了

数据库连接池

springboot1.X的数据库连接池是tomcat连接池,springboot2默认的数据库连接池由Tomcat换成 HikariCPHikariCP是一个高性能的JDBC连接池,号称最快的连接池

Druid 是阿里巴巴数据库事业部出品,为监控而生的数据库连接池,这里选取Druid作为项目的数据库连接池

maven 依赖

    <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>

设置用户名密码

spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123456

然后就可以访问 localhost:8080/druid看监控信息了

spring jdbc

maven 依赖

     <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

spring对jdbc做了封装和抽象,最常用的是 jdbcTemplateNamedParameterJdbcTemplate两个类,前者使用占位符,后者使用命名参数,我在jdbcDao做了一层简单的封装,提供统一的对外接口

jdbcDao主要方法如下:

// 占位符
find(String sql, Object... args)
// 占位符,手动指定映射mapper
find(String sql, Object[] args, RowMapper<T> rowMapper)
// 命名参数
find(String sql, Map<String, ?> paramMap)
// 命名参数,手动指定映射mapper
find(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)
//springjdbc 原queryForMap方法,如果没查询到会抛异常,此处如果没有查询到,返回null
queryForMap(String sql, Object... args)
queryForMap(String sql, Map<String, ?> paramMap)
// 分页查询
find(Page<T> page, String sql, Map<String, ?> parameters, RowMapper<?> mapper)
// 分页查询
find(Page<T> page, String sql, RowMapper<T> mapper, Object... args)

jpa

jpajava 持久化的标准,spring data jpa 使操作数据库变得更方便,需要说明的 spring data jpa 本身并不是jpa的实现,它默认使用的 providerhibernate

maven 依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

我把通用的方法抽取了出来了,封装了一个BaseRepository,使用时,直接继承该接口即可

public interface DemoRepository extends BaseRepository<Demo> {

}

BaseRepository 主要方法如下

// 新增,会对创建时间,创建人自动赋值
void saveEntity(T entity)
// 更新,会对更新时间,更新人自动赋值
void updateEntity(T entity)
// 逻辑删除
void deleteEntity(T entity)
// 批量保存
void saveEntites(Collection<T> entitys)
// 批量更新
void updateEntites(Collection<T> entitys)
// 批量逻辑删除
void deleteEntites(Collection<T> entitys)
// 根据id获取实体,会过滤掉逻辑删除的
T getById(Long id)

如果想使用传统的sql形式,可以直接使用JpaDao,为了方便使用,我尽量使JpaDao和JdbcDao的接口保持统一

JpaDao主要方法如下

// 占位符 例如:from Demo where id =?
find(String sql, Object... args)
// 命名参数
find(String sql, Map<String, ?> paramMap)
// 分页
find(Page<T> page, String hql, Map<String, ?> parameters)
// 分页
find(Page<T> page, String hql, Object... parameters)

redis

Redis 是性能极佳key-value数据库,常用来做缓存,

java 中常用的客户端有 JedisLettuce, spring data redis 是基于 Lettuce 做的二次封装

maven 依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

为了在redis读起来更方便,更改序列化方式

@Configuration
public class RedisConfig { /**
* 设置序列化方式
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
return redisTemplate;
} @Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 将类名称序列化到json串中
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<Object>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
}
}

spring cache

spring cache 抽象出了一组缓存接口,通过注解的方式使用,可以很方便的配置其具体实现,详细配置

这里使用redis做为缓存 provider, 默认的value序列化方式是JDK,为方便查看,可以修改为用json序列化

有时会有设置redis key前缀的需求,默认是这样的

	static CacheKeyPrefix simple() {
// 在 cacheName 后面添加 "::"
return name -> name + "::";
}

spring boot 提供的有配置前缀的属性

spring.cache.redis.key-prefix= # Key prefix.

但这是一个坑,这样写的效果实际这样的,会把cacheName干掉,显然不是我们想要的

CacheKeyPrefix cacheKeyPrefix = (cacheName) -> prefix;

我们想要的是把前缀加在最前面,保留cacheName

CacheKeyPrefix cacheKeyPrefix = (cacheName) -> keyPrefix + "::" + cacheName + "::";

参考org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration,声明 RedisCacheManager

@Configuration
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
public class SpringCacheConfig { @Autowired
private CacheProperties cacheProperties; @Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManagerBuilder builder = RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration());
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return builder.build();
} private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration() {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// 修改序列化为json
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
// 重写前缀拼接方式
config = config.computePrefixWith((cacheName) -> redisProperties.getKeyPrefix() + "::" + cacheName + "::");
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
// 省略 jackson2JsonRedisSerializer() }

mogodb

MongoDB 是文档型数据库,使用 spring data mogodb 可以很方便对mogodb进行操作

maven 依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

sprign data mogodb 提供了 MongoTemplate 对mogodb进行操作,我在该类的基础上又扩展了一下,可以自定义自己的方法

@Configuration
public class MongoDbConfig { /**
* 扩展自己的mogoTemplate
*/
@Bean
public MyMongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,
MongoConverter converter) {
return new MyMongoTemplate(mongoDbFactory, converter);
} }

我扩展了一个分页的方法,可以根据自己的情况扩展其它方法

// mogodb 分页
public <T> Page<T> find(Page<T> page, Query query, Class<T> entityClass)

mybatis

maven 依赖

    <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>

常用配置如下

# 配置文件位置 classpath后面要加*,不然后面通配符不管用
mybatis.mapperLocations=classpath*:com/zhaoguhong/baymax/*/mapper/*Mapper.xml
# 开启驼峰命名自动映射
mybatis.configuration.map-underscore-to-camel-case=true

dao层直接用接口,简洁,方便

@Mapper
public interface DemoMapper {
/**
* 注解方式
*/
@Select("SELECT * FROM demo WHERE user_name = #{userName}")
List<Demo> findByUserName(@Param("userName") String userName);
/**
* xml方式
*/
List<Demo> getDemos();
}

需要注意,xml的namespace必须是mapper类的全限定名,这样才可以建立dao接口与xml的关系

<mapper namespace="com.zhaoguhong.baymax.demo.dao.DemoMapper">
<select id="getDemos" resultType="com.zhaoguhong.baymax.demo.entity.Demo">
select * from demo
</select>
</mapper>

通用mapper

mybatis 的单表增删改查写起来很啰嗦,通用mapper很好的解决了这个问题

maven 依赖

    <dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>

常用配置如下

# 通用mapper 多个接口时用逗号隔开
mapper.mappers=com.zhaoguhong.baymax.mybatis.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL

定义自己的 MyMapper 方便扩展,MyMapper 接口 中封装了通用的方法,和jpaBaseRepository类似,这里不再赘述

声明mapper需要加Mapper注解,还稍显麻烦,可以用扫描的方式

@Configuration
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
public class MybatisConfig {
}

使用时直接继承MyMapper接口即可

public interface DemoMapper extends MyMapper<Demo>{
}

分页

pagehelper是一个很好用的mybatis的分页插件

maven 依赖

    <dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>

常用配置如下

#pagehelper
#指定数据库类型
pagehelper.helperDialect=mysql
#分页合理化参数
pagehelper.reasonable=true

使用分页

    PageHelper.startPage(1, 5);
List<Demo> demos = demoMapper.selectAll();

pagehelper 还有好多玩法,可以参考这里

自定义分页

pagehelper 虽然好用,但项目中有自己的分页对象,所以单独写一个拦截器,把他们整合到一起

这个地方要特别注意插件的顺序不要搞错

@Configuration
// 设置mapper扫描的包
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
@Slf4j
public class MybatisConfig { @Autowired
private List<SqlSessionFactory> sqlSessionFactoryList; /**
* 添加自定义的分页插件,pageHelper 的分页插件PageInterceptor是用@PostConstruct添加的,自定义的应该在其后面添加
* 真正执行时顺序是反过来,先执行MyPageInterceptor,再执行 PageInterceptor
*
* 所以要保证 PageHelperAutoConfiguration 先执行
*/
@Autowired
public void addPageInterceptor(PageHelperAutoConfiguration pageHelperAutoConfiguration) {
MyPageInterceptor interceptor = new MyPageInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
log.info("注册自定义分页插件成功");
}
} }

在使用时只需要传入自定义的分页对象即可

    Page<Demo> page = new Page<>(1, 10);
demos = demoMapper.getDemos(page);

spring security

安全模块是项目中必不可少的一环,常用的安全框架有shirospring security,shiro相对轻量级,使用非常灵活,spring security相对功能更完善,而且可以和spring 无缝衔接。这里选取spring security做为安全框架

maven 依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

继承WebSecurityConfigurerAdapter类就可以进行配置了

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
private SecurityProperties securityProperties; @Autowired
private UserDetailsService userDetailsService; @Override
protected void configure(HttpSecurity http) throws Exception { http
.authorizeRequests()
// 设置可以匿名访问的url
.antMatchers(securityProperties.getAnonymousArray()).permitAll()
// 其它所有请求都要认证
.anyRequest().authenticated()
.and()
.formLogin()
// 自定义登录页
.loginPage(securityProperties.getLoginPage())
// 自定义登录请求路径
.loginProcessingUrl(securityProperties.getLoginProcessingUrl())
.permitAll()
.and()
.logout()
.permitAll(); // 禁用CSRF
http.csrf().disable();
} @Override
public void configure(WebSecurity web) throws Exception {
String[] ignoringArray = securityProperties.getIgnoringArray();
// 忽略的资源,直接跳过spring security权限校验
if (ArrayUtils.isNotEmpty(ignoringArray)) {
web.ignoring().antMatchers(ignoringArray);
}
} /**
*
* 声明密码加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
} @Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService)
// 配置密码加密方式,也可以不指定,默认就是BCryptPasswordEncoder
.passwordEncoder(passwordEncoder());
} }

实现UserDetailsService接口,定义自己的UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired
private UserRepository userRepository; @Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsernameAndIsDeleted(username, SystemConstants.UN_DELETED); if (user == null) {
throw new UsernameNotFoundException("username Not Found");
}
return user;
} }

配置项

#匿名访问的url,多个用逗号分隔
security.anonymous=/test
#忽略的资源,直接跳过spring security权限校验,一般是用做静态资源,多个用逗号分隔
security.ignoring=/static/**,/images/**
#自定义登录页面
security.loginPage=/login.html
#自定义登录请求路径
security.loginProcessingUrl=/login

项目上下文

为了方面使用,封装一个上下文对象 ContextHolder

// 获取当前线程HttpServletRequest
getRequest()
// 获取当前线程HttpServletResponse
getResponse()
// 获取当前HttpSession
getHttpSession()
setSessionAttribute(String key, Serializable entity)
getSessionAttribute(String key)
setRequestAttribute(String key, Object entity)
getRequestAttribute(String key)
// 获取 ApplicationContext
getApplicationContext()
//根据beanId获取spring bean
getBean(String beanId)
// 获取当前登录用户
getLoginUser()
// 获取当前登录用户 id
getLoginUserId()
// 获取当前登录用户 为空则抛出异常
getRequiredLoginUser()
// 获取当前登录用户id, 为空则抛出异常
getRequiredLoginUserId()

单点登录

单点登录系统(SSO,single sign-on)指的的,多个系统,共用一套用户体系,只要登录其中一个系统,访问其他系统不需要重新登录

CAS

CAS(Central Authentication Service)是耶鲁大学的一个开源项目,是比较流行的单独登录解决方案。在CAS中,只负责登录的系统被称为服务端,其它所有系统被称为客户端

登录流程

  1. 用户访问客户端,客户端判断是否登录,如果没有登录,重定向到服务端去登录
  2. 服务端登录成功,带着ticket重定向到客户端
  3. 客户端拿着ticket发送请求到服务端换取用户信息,获取到后就表示登录成功

登出流程

跳转到sso认证中心进行统一登出,cas 会通知所有客户端进行登出

spring security 整合 cas

maven 依赖

    <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>

spring security 对 cas 做了很好的封装,在使用的过程中,只需要定义好对应的登录fifter和登出fifter即可,整合cas的代码我写在了WebSecurityConfig类中

相关属性配置

#是否开启单点登录
cas.enable = true
#服务端地址
cas.serverUrl=
#客户端地址
cas.clientUrl=
#登录地址
cas.loginUrl=${cas.serverUrl}/login
#服务端登出地址
cas.serverLogoutUrl=${cas.serverUrl}/logout
#单点登录成功回调地址
cas.clientCasUrl=${cas.clientUrl}/login/cas

邮件

因为要使用freeMarker解析模板,所以也要引入freeMarker依赖

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency> <dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>

相关配置

#邮件
#设置邮箱主机,163邮箱为smtp.163.com,qq为smtp.qq.com
spring.mail.host = smtp.163.com
spring.mail.username =
#授权码
spring.mail.password =
#默认的邮件发送人
mail.sender =

封装一个 MailService

   // 根据相关配置发送邮件
void sendMail(MailModel mailModel);
// 发送简单的邮件
void sendSimleMail(String to, String subject, String content);
// 发送html格式的邮件
void sendHtmlMail(String to, String subject, String content);
// 发送带附件的邮件
void sendAttachmentMail(String to, String subject, String content, String path);
// 发送带附件的html格式邮件
void sendAttachmentHtmlMail(String to, String subject, String content, String path);
// 根据模版发送简单邮件
void sendMailByTemplate(String to, String subject, String templateName,
Map<String, Object> params); }

maven

镜像

设置阿里云镜像,加快下载速度

修改 setting.xml,在 mirrors 节点上,添加

<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

也可以在项目 pom.xml 文件添加 ,仅当前项目有效

  <repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
<layout>default</layout>
</repository>
</repositories>

总结

  1. spring boot 遵循开箱即用的原则,并不需要做过多配置,网上的教程质量参差不齐,并且1.X和2.X使用时还有诸多不同,因此在使用时尽量参考官方文档
  2. 有时候默认的配置并不能满足我们的需求,需要做一些自定义配置,推荐先看一下springboot自动配置的源码,再做定制化处理
  3. 技术没有银弹,在做技术选型时不要过于迷信一种技术,适合自己的业务的技术才是最好的

基于springboot的web项目最佳实践的更多相关文章

  1. 使用IDEA新建基于SpringBoot的Web项目(超详细)

    目前java的开发很多Learner都慢慢的学习使用IDEA这款工具,个人觉得其实相比Eclipse来说差不多,个人习惯的问题,但是我还是推荐IDEA这款工具.虽然它目前是收费的,但相信网上的各种破解 ...

  2. ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...

  3. 👍SpringSecurity单体项目最佳实践

    SpringSecurity单体项目最佳实践 到这里,我们的SpringSecurity就已经完结啦,文章中可能有些地方不能做到全面覆盖,视频教程地址 初始项目地址 完成项目地址 1.搭建环境 建议下 ...

  4. 《Flask Web开发——基于Python的Web应用开发实践》一字一句上机实践(上)

    目录 前言 第1章 安装 第2章 程序的基本结构 第3章 模板 第4章 Web表单 第5章 数据库 第6章 电子邮件 第7章 大型程序的结构   前言 学习Python也有一个半月时间了,学到现在感觉 ...

  5. SVN基于Maven的Web项目更新,本地过程详细解释

    周围环境 MyEclipse:10.7 Maven:3.1.1 概要 最近在做项目,MyEclipse下载SVN基于上述Maven的Web问题,有时候搞了非常半天,Maven项目还是出现叉号,最后总结 ...

  6. springBoot 搭建web项目(前后端分离,附项目源代码地址)

    springBoot 搭建web项目(前后端分离,附项目源代码地址)   概述 该项目包含springBoot-example-ui 和 springBoot-example,分别为前端与后端,前后端 ...

  7. springboot 整合 web 项目找不到 jsp 文件

    今天遇到一个问题,就是使用springboot整合web项目的时候,怎么都访问不到 \webapp\WEB-INF\jsp\index.jsp 页面.这个问题搞了半天,试了各种方式.最后是因为在启动的 ...

  8. 基于SpringBoot的WEB API项目的安全设计

    SpringBoot的开箱即用功能,大大降低了上手一个WEB应用的门槛,友好的REST接口支持,在SpringCloud微服务体系中可编程性大大提高,本篇基于一个面向企业调用方用户的WEB API项目 ...

  9. 基于 Lerna 管理 packages 的 Monorepo 项目最佳实践

    本文首发于 vivo互联网技术 微信公众号 https://mp.weixin.qq.com/s/NlOn7er0ixY1HO40dq5Gag作者:孔垂亮 目录 一.背景二.Monorepo vs M ...

随机推荐

  1. C++学习笔记7_多态

    1. 类与类之间的关系class A{ public: int a; void funcA() {}}包含: class B { public: void funcB(){} A a; }//如果类B ...

  2. 学习笔记62_python文件操作

    #使用 str = raw_input("输入密码:") 来获取键盘的输入 #创建或打开一个文件 fp = open("hellow.txt","w& ...

  3. Numpy 中的聚合操作

    # 导包 import numpy as np sum np.random.seed(10) L = np.random.random(100) sum(L) np.sum(L) min np.min ...

  4. 康托展开&逆康托展开学习笔记

    啊...好久没写了...可能是最后一篇学习笔记了吧 题目大意:给定序列求其在全排列中的排名&&给定排名求排列. 这就是康托展开&&逆康托展开要干的事了.下面依次介绍 一 ...

  5. 线性模型之逻辑回归(LR)(原理、公式推导、模型对比、常见面试点)

    参考资料(要是对于本文的理解不够透彻,必须将以下博客认知阅读,方可全面了解LR): (1).https://zhuanlan.zhihu.com/p/74874291 (2).逻辑回归与交叉熵 (3) ...

  6. VM小技巧——虚拟机解决vm窗口太小的办法

    ——" 慢下来总结才能增大效率" 很多人在装虚拟机的时候,遇到了窗口过小不能自适应的问题.我也是查了好多资料,都说安装Vmware Tools即可解决,还有说修改分辨率也可以.两种 ...

  7. html5 点击播放video的方法

    html5 点击播放video的方法<pre> <video videosrc="{$vo.shipinurl}" controls="" x ...

  8. 两张图弄懂函数的递归(以golang为例)

    函数递归时要遵守的原则: 执行一个函数时,就要创建一个新的受保护的独立空间(新函数栈) 函数的局部变量是独立的,不会相互影响: 递归必须向退出递归的条件逼近,否则就会无限递归: 当一个函数执行完毕,或 ...

  9. Python 基础之 I/O 模型

    一.I/O模型 IO在计算机中指Input/Output,也就是输入和输出.由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘.网络等,就需要IO接 ...

  10. CentOS7 编码编译安装或卸载http2.4.25 一键脚本

    待完善 CentOS 7测试 哈哈 #!/bin/bash #************************************************************** #Autho ...