基于springboot开发javaweb项目,最最常用的功能无外乎一个写controller的springmvc与数据库相关框架了,javaweb开发往往离不开这两块框架,实际工作中代码工作量最多的工作也就在这两块。
spring-boot-starter-web
,针对web项目的安全框架spring-boot-starter-security
也是基于web项目扩展的。jdbc
(java database connectivity),基本上java大部分数据库框架都基于此开发,在jdbc的基础上,oracle(实际上是原来的sun)制定了JPA(Java Persistence API)
规范,目前最主流的基于jpa规范的orm框架是hibernate,也是spring-data-jpa底层框架。spring-data-redis
springboot3.0
,想用之前的版本请转old
分支查看mvn install
,在其他项目中引入,目前我在尝试引入到中央仓库,后续就不需要在进行导入了,直接使用即可<dependency>
<groupId>top.codef</groupId>
<artifactId>ameba-spring-boot-starter</artifactId>
<version>0.4.0</version>
</dependency>
下面就依次进行几个模块的介绍与使用
java的orm框架很多,最常用的两个:
Hibernate和MyBatis都是Java中比较流行的ORM框架,但它们在实现上有很大的区别。以下是Hibernate和MyBatis的区别:
编程范式:Hibernate是基于JPA规范实现的ORM框架,它采用的是面向对象编程的范式。而MyBatis是一个半自动化的持久化框架,它采用的是基于SQL的编程范式。
映射方式:Hibernate通过注解或XML配置文件将Java类和数据库表进行映射,可以实现自动映射和一些高级映射,例如继承映射、多对多关系等。而MyBatis使用XML配置文件将SQL语句和数据库表进行映射,开发人员需要手动编写SQL语句。
性能:Hibernate会对整个对象图进行处理,可能会产生大量的SQL语句,从而导致性能瓶颈。而MyBatis只会执行映射文件中定义的SQL语句,可以精确控制SQL执行的效率。
数据库支持:Hibernate支持多种数据库,包括关系型数据库和NoSQL数据库。而MyBatis只支持关系型数据库。
灵活性:MyBatis提供了很大的灵活性,可以编写自定义的SQL语句和映射规则,可以满足各种特殊需求。而Hibernate更适合开发基于对象的应用程序,提供了更高层次的抽象和自动化。
这里框架选择hibernate主要有三个方面原因
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String phone;
private String password;
private String username;
}
import org.springframework.stereotype.Repository;
import top.codef.dao.BaseDao;
@Repository
public class UserDao extends BaseDao {
}
这就完事了?没错,BaseDao是一个抽象类,里面包含了很多数据库相关操作方法,具体方法后面会详细介绍
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.havefun.user.dao.UserDao;
import com.havefun.user.entities.User;
import jakarta.transaction.Transactional;
@Service
@Transactional
public class UserService {
@Autowired
private UserDao userDao;
public User create(User user) {
userDao.create(user);
return user;
}
public User modify(User user) {
userDao.merge(user);
return user;
}
public User get(Long id) {
return userDao.get(User.class, id);
}
public void Delete(Long id) {
userDao.delete(id);
}
}
application.properties
或者application.yaml
配置中配置数据连接,其实就是spring data jpa的相关连接,当然还需要在对应的数据中创建相关的数据表信息spring:
application:
name: spring-boot-fun
jpa:
database: mysql
show-sql: true
properties:
hibernate:
'[format_sql]': true
'[default_batch_fetch_size]': 10
datasource:
username: your username
password: your pwd
url: jdbc:mysql://127.0.0.1:3306/have_fun?useSSL=false&serverTimezone=GMT%2b8
driver-class-name: com.mysql.cj.jdbc.Driver
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.havefun.user.entities.User;
import com.havefun.user.services.UserService;
@SpringBootTest
class Springboot3ApplicationTests {
@Autowired
private UserService userService;
@Test
void contextLoads() {
userService.create(new User("phone num", "raw pwd", "some name"));
}
}
Hibernate:
insert
into
user
(password, phone, username)
values
(?, ?, ?)
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.havefun.user.services.UserService;
@SpringBootTest
class Springboot3ApplicationTests {
@Autowired
private UserService userService;
@Test
void contextLoads() {
var user = userService.get(1L);
System.out.println(user);
}
// Hibernate:
// select
// u1_0.id,
// u1_0.password,
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// where
// u1_0.id=?
// User [id=1, phone=phone num, password=raw pwd, username=some name]
}
以上就是最简单的一个例子。
通常情况下,对于处理SQL不会那么简单,可能会稍微的复杂一些,JPA中提供了一套完整的去SQL化查询机制:Criteria SQL,由hibernate进行了实现,Criteria去进行处理相对于直接写SQL会显得有些复杂,不过好处是其良好的二次封装特性。本人就在criteria的基础上进行的二次封装。
public User getUserByPhone(String phone) {
var user = userDao.getSingle(User.class, QueryBuilder.createFilter().eq("phone", phone));
return user;
}
// Hibernate:
// select
// u1_0.id,
// u1_0.password,
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// where
// u1_0.phone=?
// User [id=1, phone=phone num, password=raw pwd, username=some name]
其中QueryBuilder
中提供了一个静态方法:
public static CommonFilter createFilter() {
return new CommonFilter();
}
其中CommonFilter
里面包含了一个SQL语句中需要的一些必要组成部部分,例如
1. where条件相关
2. order相关
3. groupby相关
4. select相关
5. update相关
6. join相关
等等,根据这些组成部分,来进行Criteria的组装,最终得到想要的数据结果,下面就是CommonFilter中支持的where的一些操作
操作 | 方法 |
---|---|
where相关 | |
相等 | eq |
不等 | neq |
近似 | like |
为null | isNull |
不为null | isNotNull |
包含 | in |
不包含 | notIn |
大于 | gt |
大于等于 | ge |
小于 | lt |
小于等于 | le |
在……之间 | between |
BaseDao
基本上涵盖了大部分Criteria操作,常用的增删改查都能通过BaseDao内的方法来实现,下面列举出BaseDao的一些操作:方法名 | 说明 |
---|---|
getSingle | 获取单个实例 |
getList | 获取列表数据 |
getPage | 查询分页 |
count | 获取数量 |
countDistinct | 获取数量(去重) |
update | update操作 |
updateWithNull | update操作(可传null) |
delete | delete操作(条件删除) |
除了上述的一些方法外,BaseDao
还继承了AbstractDao
的操作,AbstractDao
中包含了最基础的CRUD相关操作(create、merge、detach、delete、get,flush、refresh,lock),同时可以获取当前数据操作对应的EntityManager
与Session
(hibernate)。
CommonDao
bean来作为一个公共的Dao组件,其他组件可以直接进行调用 @Autowired
private CommonDao commonDao;
public User getUserByPhone(String phone) {
var user = userDao.getSingle(User.class, QueryBuilder.createFilter().eq("phone", phone));
return user;
}
表明了通过条件查询单个实体信息可以像上述方法那样做,需要注意的是,通过getsingle
方法查询出的结果包含多条的话,会报错:
org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 2
/**
* 通过手机号获取用户名
*
* @param phone
* @return
*/
public String getUserNameByPhone(String phone) {
var username = userDao.getSingle(String.class, User.class,
QueryBuilder.createFilter().select("username").eq("phone", phone));
return username;
}
// Hibernate:
// select
// u1_0.username
// from
// user u1_0
// where
// u1_0.phone=?
// some name
通过QueryBuilder.createFilter().select("实体类字段名")
的方式就可以进行某个字段的查询了,多字段查询时一样的方式,例如需要通过手机号获取没有password的用户信息,做到如下几步就可实现:
1.可以在User
中添加构造器:
public User(String phone, String username) {
super();
this.phone = phone;
this.username = username;
}
QueryBuilder
构建CommonFilter
,然后调用select("参数1","参数2")
public User getReadUser(String phone) {
var user = userDao.getSingle(User.class,
QueryBuilder.createFilter().select("phone", "username").eq("phone", phone));
return user;
}
// Hibernate:
// select
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// where
// u1_0.phone=?
// User [id=null, phone=phone num, password=null, username=some name]
方式也非常简单。
User
返回,可以新建一个新类来承载返回的数据:import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ReadUser {
private String phone;
private String username;
public ReadUser(String phone, String username) {
this.phone = phone;
this.username = username;
}
}
具体调用与上面的的方法会有点区别:
public ReadUser getReadUser(String phone) {
var user = userDao.getSingle(ReadUser.class, User.class,
QueryBuilder.createFilter().select("phone", "username").eq("phone", phone));
return user;
}
这些说明很重要,需要注意
QueryBuilder
构建CommonFilter
时,假如通过类似eq
,lt
等判断方法时,若其传的参数为null,代码则认为此条件为无效条件,例如: public List<User> getList(String phone) {
var list = userDao.getList(User.class, QueryBuilder.createFilter().eq("phone", phone));
return list;
}
假如传入的phone
为null
时,构建的查询语句不会存在where phone = ...
@Test
void nullTest() {
userService.getList(null).forEach(System.out::println);
// Hibernate:
// select
// u1_0.id,
// u1_0.password,
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// User [id=1, phone=phone num, password=raw pwd, username=some name]
// User [id=2, phone=2, password=2, username=2]
}
这样做的目的在于大多数查询筛选过程中会把筛选条件当成非必填参数进行传递,一般情况下非必填参数是不需添加where
相关操作的。
getPage
,例如:public <T> Page<T> getPage(Class<T> clazz, Pageable pageable, CommonFilter filter)
其中Pageable
结构如下:
public class Pageable {
private Long pageCount;// 页数
private int eachPageSize = 15;// 每页大小
private int pageNo = 1;// 页码
private OrderEnum order = OrderEnum.DESC;// 排序方式
private String orderStr = "id";// 排序字段名
private Long totalCount; //总数
//getter and setter
}
需要说明一下:页码是从第一页开始,每页大小默认为15个,默认情况下,数据分页是按照id
字段倒排序获取,查询完成后会回填pageCount
页数与totalCount
数据总数;返回值是Page<T>
对象:
public class Page<T> {
private Pageable pageable;
private List<T> content = new ArrayList<T>(0);
public Page(Pageable pageable) {
this.pageable = pageable;
}
public Page() {
}
}
基本上pageable
对应的参数足够使用,假如说还有其他相关需求可以发布issue讨论,具体page相关操作可以这么做:
public Page<User> getPage(Pageable pageable) {
Page<User> userPage = userDao.getPage(User.class, pageable, QueryBuilder.createFilter().gt("id", 2));
return userPage;
}
// Hibernate:
// select
// u1_0.id,
// u1_0.password,
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// where
// u1_0.id>?
// order by
// u1_0.id desc limit ?,
// ?
// Hibernate:
// select
// count(u1_0.id)
// from
// user u1_0
// where
// u1_0.id>?
public List<User> getSortList() {
List<User> list = userDao.getList(User.class,
QueryBuilder.createFilter().orderByAsc("phone", "username").orderByDesc("id"));
return list;
// Hibernate:
// select
// u1_0.id,
// u1_0.password,
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// order by
// u1_0.phone asc,
// u1_0.username asc,
// u1_0.id desc
}
getPage
方法中的Pageable
也有排序的字段,在查询过程中会优先把分页中的排序放在第一位
@JoinColumn
进行操作的,例如,上面给出了User
,同时UserAuth
与User
成多对一关系@Entity
@Getter
@Setter
public class UserAuth {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
private String authName;
public UserAuth(User user, String authName) {
this.user = user;
this.authName = authName;
}
public UserAuth() {
}
UserAuth
的操作,基本上应该是这样@Service
@Transactional
public class AuthService {
@Autowired
private UserDao userDao;
public UserAuth create(Long userId, String authName) {
var user = userDao.get(User.class, userId);
if (user == null)
throw new HaveReasonException("无此用户");
var auth = new UserAuth(user, authName);
userDao.create(auth);
return auth;
}
}
//Hibernate:
// select
// u1_0.id,
// u1_0.password,
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// where
// u1_0.id=?
//Hibernate:
// insert
// into
// user_auth
// (auth_name, user_id)
// values
// (?, ?)
User
中的phone字段查询出Auth
中的authName
字段,如下: public List<String> getAuthNameByUserPhone(String phone) {
var list = userDao.getList(String.class, UserAuth.class,
QueryBuilder.createFilter().eq("user.phone", phone).select("authName"));
return list;
}
// Hibernate:
// select
// u1_0.auth_name
// from
// user_auth u1_0
// join
// user u2_0
// on u2_0.id=u1_0.user_id
// where
// u2_0.phone=?
// [auth1, auth2]
注意这里用到的是user.phone
,这里表示的是UserAuth
中的user
字段对应的User
Entity实体中的phone字段,是不是很简单呢?
Auth
中,我想知道每个用户分配的权限数量,我来定义一个新的实体类:import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AuthCount {
private String username;
private Long authCount;
public AuthCount(String username, Long authCount) {
this.username = username;
this.authCount = authCount;
}
}
这里需要用到前面建立的UserAuth
实体类,需要查询到User
中的username
,和每个User
中的实体数量:
public List<AuthCount> getAuthCountList() {
var list = userDao.getList(AuthCount.class, UserAuth.class, QueryBuilder.createFilter().select(Selectors.select("user.username"), Selectors.count("id")).groupBy("user"));
return list;
}
// Hibernate:
// select
// u2_0.username,
// count(u1_0.id)
// from
// user_auth u1_0
// join
// user u2_0
// on u2_0.id=u1_0.user_id
// group by
// u1_0.user_id
Selectors
是一个静态类,里面包含了一些常用的聚合方法例如count/avg/max/min/abs等等,通常用于select时的一些聚合处理@JoinColumn
与@ManyToOne
(OneToOne等)处理实现的,跟你需要的数据结构有关import org.springframework.stereotype.Repository;
import top.codef.dao.BaseDao;
@Repository
public class UserDao extends BaseDao {
}
可以把BaseDao
替换成TypedBaseDao
import org.springframework.stereotype.Repository;
import top.codef.dao.TypedBaseDao;
@Repository
public class TypedUserDao extends TypedBaseDao{
}
此种方式可以通过函数式的方式进行查询:
@SuppressWarnings("unchecked")
public List<User> getUserbyFunctional(String phone) {
List<User> list = typedUserDao.getList(QueryBuilder.createFilter(User.class)
.select(User::getPhone, User::getUsername).eq(User::getPhone, phone));
return list;
}
// Hibernate:
// select
// u1_0.phone,
// u1_0.username
// from
// user u1_0
// where
// u1_0.phone=?
目前此功能只在试验阶段,可以简单的使用。
spring-boot-starter-data-redis
的基础上对StringRedisTemplate
进行了一次工具化封装,做到即插即用(需要继承一下)假设有一个邀请码需求需要用到redis
的Value
结构:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import top.codef.redisdao.AbstractValueDao;
@Repository
public class CodeValueDao extends AbstractValueDao {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String getKey(String key) {
return "your_key_preffix:".concat(key);
}
@Override
public StringRedisTemplate getStringRedisTemplate() {
return stringRedisTemplate;
}
}
其中AbstractValueDao
是一个抽象类,里面包含了常用的value
相关操作,需要使用时直接继承即可,但是需要实现两个方法:
public String getKey(String key)
表示redis中存储的键public StringRedisTemplate getStringRedisTemplate()
表示需要使用的StringRedisTemplate对象value
结构,还提供很多种容器类结构(set、list、hash),这类结构在定义redis键时需要分为两种:AbstractHashDDao
,键固定的set存储:AbstractSetCDao
。import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import top.codef.redisdao.AbstractSetCDao;
@Repository
public class PhoneSetCDao extends AbstractSetCDao {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String getKey() {
return "your_key_preffix:phone_set";
}
@Override
public StringRedisTemplate getStringRedisTemplate() {
return stringRedisTemplate;
}
}
使用时,可以直接这样调用即可:
@Autowired
private PhoneSetCDao phoneSetCDao;
public boolean isPhoneExist(String phone) {
return phoneSetCDao.isMember(phone);
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import top.codef.redisdao.AbstractSetDDao;
@Repository
public class CompanyPhoneSetDao extends AbstractSetDDao {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String getKey(String key) {
return "your_key_preffix:".concat(key);
}
@Override
public StringRedisTemplate getStringRedisTemplate() {
return stringRedisTemplate;
}
}
调用时是这样:
@Autowired
private CompanyPhoneSetDao companyPhoneSetDao;
public boolean isCompanyHasPhone(String company, String phone) {
return companyPhoneSetDao.isMember(company, phone);
}
需要注意:companyPhoneSetDao.isMember(company, phone);
方法中传的company
参数最终是通过public String getKey(String key)
进行了一次修饰,相当于redis查询时是通过your_key_preffix:company
来进行查询的。
其实对于redis键定义,我强烈建议redis的键做统一的处理,本人在处理redis的键时,会建立一个enum
结构来进行统一管理:
public enum RedisKeyEnum {
PHONE_SET("key_preffix:phone_set", "这是个手机号的"), COMPANY_SET("company_key_preffix", "这是company手机号");
private final String key;
private final String explain;
private RedisKeyEnum(String key, String explain) {
this.key = key;
this.explain = explain;
}
public String getKey() {
return key;
}
public String getExplain() {
return explain;
}
public String getKey(String key) {
return String.format("%s:%s", this.key, key);
}
}
键值统一管理后,通过继承AbstractXXXDao时可以这样调用
//DDao相关
@Override
public String getKey(String key) {
return RedisKeyEnum.COMPANY_SET.getKey(key);
}
//CDao相关
@Override
public String getKey() {
return RedisKeyEnum.PHONE_SET.getKey();
}
这样做的好处防止redis键的混乱调用可能导致的键名重复问题,通知根据其命名可以清楚的知道其用的redis结构是什么。
在架构自己的业务时,处理异常也是主要日常工作之一,实际上我们在处理异常时要分为两种:
目前架构中已经约定了restful风格的相关异常:
@RestControllerAdvice
public class GlobalExceptionHandler {
protected final Log logger = LogFactory.getLog(getClass());
@ExceptionHandler
@ResponseStatus(value = HttpStatus.EXPECTATION_FAILED)
public StatusResultModel reasonErro(HaveReasonException e) {
logger.info("业务异常" + e.getMessage(), e);
return ResponseStatusEnum.REASONERRO.createStatusModel(e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.I_AM_A_TEAPOT)
public StatusResultModel uncaughtError(Exception exception, HttpServletRequest request) {
logger.error("未知错误", exception);
return ResponseStatusEnum.FAIL.createStatusModel("系统出错");
}
}
其中HaveReasonException
是框架内定义的业务异常,假如抛出此异常后,请求返回会返回错误信息,响应状态码为417,若是其他异常,则是直接返回信息系统出错,同时响应状态码为418。
// some service
public void someReasonError() {
throw new HaveReasonException("这是个异常错误");
}
//some controller
@PostMapping("/errorsome")
public void errorSome() {
userService.someReasonError();
}
想要开启此异常返回,只需要在application.yml
中做如下配置:
ameba:
enable-error-advice: true
微服务间的异常统一其实也很重要,通过spring提供的spring-cloud-starter-openfeign
调用微服务时,被调用的微服务也可以使用上面的约定(417错误、418错误),对于417错误(业务异常),可能需要做到异常透传,即把微服务异常的信息返回给终端客户,这是就需要进行fein相关的约定配置:
public class FeignErrorDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper;
private Log logger = LogFactory.getLog(FeignErrorDecoder.class);
public FeignErrorDecoder(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public Exception decode(String methodKey, Response response) {
int code = response.status();
if (code == 417) {
String message = getMessage(response);
return new HaveReasonException(message == null ? "error" : message);
} else {
logger.error("call error:" + code);
return FeignException.errorStatus(methodKey, response);
}
}
private String getMessage(Response response) {
try {
JsonNode jsonNode = objectMapper.reader().readTree(response.body().asReader(StandardCharsets.UTF_8));
String message = jsonNode.get("message").asText("");
logger.warn("feign client call error:" + message);
return message;
} catch (Exception e) {
logger.debug("response body resolve error!", e);
return null;
}
}
}
这样就能做到微服务间的异常约定统一
除了上述的异常处理。给终端返回restful请求结果进行一个包装,框架内包含了一个常用的包装约定enum:
public enum ResponseStatusEnum {
SUCCESS(0, "操作成功", "操作成功,默认返回操作"), FAIL(-1, "操作失败", "操作失败"), PARAMERRO(1, "参数错误,请检查参数是否正确", "参数验证模块处理"),
SQLERROR(2, "数据存取错误", "hibernate错误"), NULLERROR(2, "空指针错误", "空指针异常"), USERVALIDATEERRO(302, "用户验证异常,请重新登录", ""),
REASONERRO(10, "操作起因问题,请联系管理员", "此方式可以重写message"), TIMEOUTERROR(408, "操作超时", "");
private final int status;
private final String message;
private final String explain;
private ResponseStatusEnum(int status, String message, String explain) {
this.status = status;
this.message = message;
this.explain = explain;
}
public StatusResultModel createStatusModel() {
StatusResultModel model = new StatusResultModel();
model.setMessage(message);
model.setStatus(status);
return model;
}
public StatusResultModel createStatusModel(String message) {
StatusResultModel model = new StatusResultModel();
model.setMessage(message);
model.setStatus(this.status);
return model;
}
public <T> ResultModel<T> createResultModel(T result) {
ResultModel<T> re = new ResultModel<>();
re.setMessage(message);
re.setStatus(status);
re.setResult(result);
return re;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
public String getExplain() {
return explain;
}
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。