字数:21669
1 引言
springboot的一个常规示例,主要包含几点功能:
-
2.1 spring boot main启动
-
2.2 统一的异常处理
-
2.3 基类entity
-
2.4 表单form bean与entity bean分开
-
2.5 Json工具类
-
2.6 redis template
-
2.7 mqtt集成
-
2.8 swagger2集成
-
2.9 spring data jpa介绍
-
2.10 几个小配置
-
2.11 @JsonView的应用
-
2.12 @JsonProperty的应用
-
2.13 统一加返回信息的code/error message
-
2.14 @Cacheable的应用
代码结构如下:
└─com
└─xx
├─exception
│ CustomizedResponseEntityExceptionHandler.java #集中异常处理
│ ErrorDetails.java #异常信息自定义
│
└─qingdu
│ Starter.java #main启动
│
├─bean
│ │ AbsIdEntity.java #抽象entity bean
│ │ LibraryCard.java #具体entity bean
│ │ LibraryCardExt.java #具体entity bean
│ │ UserBase.java #具体entity bean
│ │ UserByPhone.java #具体entity bean
│ │ UserByWechat.java #具体entity bean
│ │
│ └─frm
│ FrmLibraryCard.java #form bean
│ FrmNewLibraryCard.java #form bean
│ FrmUserByWechat.java #form bean
│
├─comm
│ CommConst.java #公共参数
│ JsonUtil.java #json工具类
│ Utils.java #工具类,主要是集中注入redis/mqtt/id generator等
│
├─config
│ IdGeneratorConfig.java #ID generator实例化
│ JpaConfig.java #spring data jpa实例化配置
│ MqttConfig.java #mqtt实例化
│ RedisConfig.java #redis实例化
│ Swagger2Configuration.java #swagger实例化
│
├─controller
│ LibraryCardController.java #控制类
│ UserController.java #控制类
│
├─repository
│ │ LibraryCardExtRepository.java #DAO接口
│ │ LibraryCardRepository.java #DAO接口
│ │ UserBaseRepository.java #DAO接口
│ │ UserByPhoneRepository.java #DAO接口
│ │ UserByWechatRepository.java #DAO接口
│ │
│ └─base
│ BaseRepository.java #基础DAO接口
│ BaseRepositoryFactoryBean.java #基础dao接口实例化工厂
│ BaseRepositoryImpl.java #基础DAO接口的实现类,覆盖了上层接口的方法
│
└─service
│ LibraryCardService.java #服务接口
│ UserByPhoneService.java #服务接口
│ UserByWechatService.java #服务接口
│
├─impl
│ LibraryCardServiceImpl.java #服务接口实现类
│ UserByPhoneServiceImpl.java #服务接口实现类
│ UserByWechatServiceImpl.java #服务接口实现类
│
└─mqtt
MqttMessageConsumer.java #mqtt接收消息干活类
MqttMessageProducer.java #mqtt发送消息干活类
2.1 spring boot main启动
只要加上关键注解即可
@SpringBootApplication //main方式启动
public class Starter {
public static void main(String[] args) {
SpringApplication.run(Starter.class, args);
}
}
2.2 统一的异常处理
要继承ResponseEntityExceptionHandler,参考这里。
@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), "Validation Failed",
ex.getBindingResult().toString());
return new ResponseEntity(errorDetails, HttpStatus.BAD_REQUEST);
}
}
更多的异常统一处理方式见这里
2.3 基类entity
为什么要基类entity,因为我打算统一处理id、新建、更新时间戳、逻辑删除,如果后续mysql -> elasticsearch,同步时或许需要用到更新时间戳。
@MappedSuperclass
public abstract class AbsIdEntity implements Serializable {
@Id
@Column(name="id")
protected long id;
@Column(name = "create_time")
protected Timestamp createTime;
@Column(name = "last_time")
protected Timestamp lastTime;
@Column(name = "is_delete")
protected int isDelete;
}
2.4 表单form bean与entity bean分开
form bean和entity bean分开,会造成写大量的form bean,为什么分开?原因是:
-
form bean的用法基本上是,现有数据entity bean的同名属性,如果在form bean中有值则覆盖
后来在实践中有两条比较方便的做法:
(1)即便两者都有属性,也可以设定不覆盖, 通过BeanUtils.copyProperties
;
(2)com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper().readValue(json, cls)
在json串转对象时,对象有多的属性也没OK -
entity bean的属性更丰富,依照第1条,如果被Form bean滥用,关键数据可能会不安全
后来在实践中有办法:即2.11,用JsonView注解 -
在用swagger生成参数帮助时,它根据bean来生成json,这又加剧了第2条发生的概率
后来在实践中有解决办法:**防止Formbean的属性把entity bean的覆盖?**用BeanUtils.copyProperties(userMini, exists, new String[] {"id", "idUnion", "createTime", "isDelete"})
, 第三个参数就是解决哪些字段不要拷贝。
2.5 Json工具类
这个工具类的主要作用就是:
-
将json串与对象互转,目前在springmvc的框架下,这个都不怎么用得上了,当然在与第三方接口交互时存在需求。
-
另一点就是替换json串的字段值,为什么?因为在使用MQTT发消息时,消息用的是JSON模板,模板里的内容还是要适时变化一下的。
此工具类的内容如下,参考自:这里:
public class JsonUtil {
private static ObjectMapper mapper = new ObjectMapper();
static {
mapper.setSerializationInclusion(Include.NON_NULL);
//设置输入时忽略JSON字符串中存在而Java对象实际没有的属性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public static void main(String[] args) {
User u = new User();
u.setName("tyc");
u.setAge(10);
System.out.println(JsonUtil.object2Json(u));
System.out.println(JsonUtil.json2Object("{\"name\":\"daniel\",\"age\":12,\"notexists\":true}", User.class));
System.out.println(JsonUtil.replaceFieldsOfJsonstr("{\"name\":\"daniel\",\"age\":12,\"notexists\":true}"
, new String[][] {{"name","daniel1"}, {"age", "11"}, {"kk", "22"}}));
}
static class User{
String name;
String pwd;
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return String.format("name:%s, pwd:%s, age:%d",
name == null ? "" : name,
pwd == null ? "" : pwd,
age);
}
}
@SuppressWarnings("unchecked")
public static Map<String, String> jsonstr2Map(String jsonStr) {
try {
return mapper.readValue(jsonStr, HashMap.class);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 替换json string中的字段值
*
* https://blog.csdn.net/qq_37936542/article/details/79268402
* @param jsonStr 串
* @param keyVal 键值对的二维数组
* @return
*/
public static String replaceFieldsOfJsonstr(String jsonStr, String[]... keyVals) {
if (keyVals == null || keyVals.length == 0) return jsonStr;
try {
@SuppressWarnings("unchecked")
HashMap<String, String> map = mapper.readValue(jsonStr, HashMap.class);
for (String[] keyVal : keyVals) {
if (map.containsKey(keyVal[0])) {
map.put(keyVal[0], keyVal[1]);
}
}
return mapper.writeValueAsString(map);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 将对象转化成json string
* @param o 对象
* @return
*/
public static String object2Json(Object o) {
if (o == null)
return null;
String s = null;
try {
s = mapper.writeValueAsString(o);
} catch (Exception e) {
e.printStackTrace();
}
return s;
}
/**
* 将对象列表转化为json string列表
* @param objects 对象列表
* @return
*/
public static <T> List<String> listObject2ListJson(List<T> objects) {
if (objects == null)
return null;
List<String> lists = new ArrayList<String>();
for (T t : objects) {
lists.add(JsonUtil.object2Json(t));
}
return lists;
}
/**
* 将json string列表转化为对象列表
* @param jsons json string列表
* @param c 类
* @return
*/
public static <T> List<T> listJson2ListObject(List<String> jsons, Class<T> c) {
if (jsons == null)
return null;
List<T> ts = new ArrayList<T>();
for (String j : jsons) {
ts.add(JsonUtil.json2Object(j, c));
}
return ts;
}
/**
* 将json string转化为对象
* @param json
* @param c
* @return
*/
public static <T> T json2Object(String json, Class<T> c) {
if (StringUtils.isEmpty(json))
return null;
T t = null;
try {
t = mapper.readValue(json, c);
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
@SuppressWarnings("unchecked")
public static <T> T json2Object(String json, TypeReference<T> tr) {
if (StringUtils.isEmpty(json))
return null;
T t = null;
try {
t = (T) mapper.readValue(json, tr);
} catch (Exception e) {
e.printStackTrace();
}
return (T) t;
}
}
2.6 redis template
这个配置类目前还未完成,大概内容,比较奇怪的一点是实例化ReidsTemplate对象的代码注释后,还是可用:
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
//缓存管理器,扩展
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheManager cacheManager = RedisCacheManager.create(factory);
//设置缓存过期时间
//cacheManager.setDefaultExpiration(10000);
return cacheManager;
}
/*//缓存管理器,扩展
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置缓存过期时间
cacheManager.setDefaultExpiration(10000);
return cacheManager;
}*/
@Bean
@Override
public KeyGenerator keyGenerator() {
//未解决的问题:缓存中的内容如何同步更新?
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(".").append(method.getName()).append("_"); //不能用冒号?
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/*@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
//设置序列化工具
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
@SuppressWarnings({ "rawtypes", "unchecked" })
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisTemplate template = new StringRedisTemplate(factory);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}*/
}
用法就是在uitls统一注入:
@Component
public class Utils {
@Autowired private SnowflakeIdWorker idGenerator;
@Autowired private RedisTemplate<String, String> redisTemplate;
@Autowired private MqttMessageProducer mqttMessageProducer;
public SnowflakeIdWorker idGenerator() {
return idGenerator;
}
public MqttMessageProducer mqttProducer() {
return this.mqttMessageProducer;
}
public RedisTemplate<String, String> redisTemplate(){
return redisTemplate;
}
public <T extends AbsIdEntity> T createEntity(Class<T> cls) {
T entity = null;
try {
entity = cls.newInstance();
entity.setId(idGenerator.nextId());
entity.setCreateTime(new Timestamp(System.currentTimeMillis()));
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return entity;
}
/**
* 将缓存中的内容取出,并转化为bean返回
* @param key cache中键
* @param cls bean类
* @return
*/
public <T extends AbsIdEntity> T getEntityFromCache(String key, Class<T> cls) {
String jsonStr = redisTemplate.opsForValue().get(key);
if (jsonStr != null) {
return JsonUtil.json2Object(jsonStr, cls);
}
return null;
}
}
2.7 mqtt集成
mqtt是一个消息框架,springboot中可以很方便地集成,引入依赖:
<!-- mqtt -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
初始化方法如下:
@Configuration
@IntegrationComponentScan
public class MqttConfig {
//为什么要定义两个id(serverId/clientId)?
@Value("${spring.mqtt.server.id}")
private String serverId;
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
@Value("${spring.mqtt.url}")
private String hostUrl;
@Value("${spring.mqtt.client.id}")
private String clientId;
@Value("${spring.mqtt.default.topic}")
private String defaultTopic;
@Autowired private MqttMessageConsumer mqttMessageConsumer;
//-----------------consumer
//channel adapter通道适配器是单向的,入站通道适配器只支持接收消息,出站通道适配器只支持输出消息\
//channel adapter属于message endpoint,后者与Message、channel同属于spring integration的三大部分
// ,以达到不同系统之间交互的问题,通过异步消息驱动来达到系统交互时系统之间的松耦合
@Bean
public MessageProducerSupport mqttInbound(MqttPahoClientFactory mqttClientFactory) {
//可以订阅多个topic
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(serverId,
mqttClientFactory, new String[]{
CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ + "+",
CommConst.TOPIC_SUBCRIBE_CARD_REG_POST_CARD + "+",
CommConst.TOPIC_SUBCRIBE_SCAN_LOGIN_CODE_SPEC_REQ + "+"
}
) ;
adapter.setCompletionTimeout(20000);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(2);
return adapter;
}
//IntegrationFlow来定义系统继承流程,
//通过IntegrationFlows和IntegrationFlowBuilder来实现使用Fluent API来定义流程
//可分流: payload -> category -> channel
@Bean
public IntegrationFlow mqttInFlow(MqttPahoClientFactory mqttClientFactory) {
return IntegrationFlows.from(mqttInbound(mqttClientFactory))
//.transform(p -> p + ", received from MQTT")
//.handle(logger())
.handle(mqttMessageConsumer)
.get();
}
//暂时没用
// private LoggingHandler logger() {
// LoggingHandler loggingHandler = new LoggingHandler("INFO");
// loggingHandler.setLoggerName("siSample");
// return loggingHandler;
// }
//-----------------producer
/**
* The Service Activator is the endpoint type for connecting any Spring-managed Object to an input channel
* so that it may play the role of a service. If the service produces output, it may also be connected to
* an output channel. Alternatively, an output producing service may be located at the end of a processing
* pipeline or message flow in which case, the inbound Message's "replyChannel" header can be used.
* This is the default behavior if no output channel is defined, and as with most of the configuration options
* you'll see here, the same behavior actually applies for most of the other components we have seen.
*
* refer to:
* https://docs.spring.io/spring-integration/docs/2.0.0.RC1/reference/html/service-activator.html
*
* @return
*/
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound(MqttPahoClientFactory mqttClientFactory) {
//这里不用new DefaultMqttPahoClientFactory(),因spring本身已经初始化这个Factory对象,加个参数就行了
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(clientId, mqttClientFactory);
messageHandler.setAsync(true);
messageHandler.setDefaultTopic(defaultTopic);
messageHandler.setDefaultQos(2);
return messageHandler;
}
// 暂无用途
// @Bean
// public MessageChannel mqttOutboundChannel() {
// return new DirectChannel();
// }
//-----------------public
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setUserName(username);
factory.setPassword(password);
factory.setServerURIs(new String[]{hostUrl});
factory.setKeepAliveInterval(2);
return factory;
}
}
使用也是很简单:
@Component
public class MqttMessageConsumer implements MessageHandler{
final org.slf4j.Logger log = LoggerFactory.getLogger(MqttMessageConsumer.class);
@Autowired Utils utils;
@Autowired private MqttMessageProducer mqttMessageProducer;
@Autowired private LibraryCardRepository libraryCardRepository;
@Override
public void handleMessage(Message<?> message) throws MessagingException {
MessageHeaders headers = message.getHeaders();
String topic = headers.get("mqtt_topic", String.class);
String content = (String)message.getPayload();
log.debug(content);
// 终端请求办证二维码规范
if (topic.startsWith(CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ)) {
// 生成业务参考号,并将业务办理缓存
String refID = String.valueOf(utils.idGenerator().nextId());
String reply = JsonUtil.replaceFieldsOfJsonstr(
CommConst.JSON_CARD_REG_CODE_SPEC, new String[][] {{"refID", refID}});
// 在redis中注册业务,并有时限:15分钟
utils.redisTemplate().opsForValue().set(refID, content, CommConst.TIMEOUT_MINUTES, TimeUnit.MINUTES);
// 发送二维码规范
mqttMessageProducer.sendToMqtt(reply
, CommConst.TOPIC_PUBLISH_CARD_REG_CODE_SPEC
// + 设备号
+ topic.replace(CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ, ""));
// 终端发卡结果
} else if (topic.startsWith(CommConst.TOPIC_SUBCRIBE_CARD_REG_POST_CARD)) {
//TODO 测试一下,可能存在json string转对象,即使String非对象的内容也能生成了一对象
// 成功即将已缓存的成功数据保存到数据库中
if ("true".equals(JsonUtil.jsonstr2Map(content).get("result"))) {
// 更新发卡成功的数据到数据库
LibraryCard entity = utils.getEntityFromCache(
topic.replace(CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ, ""), LibraryCard.class);
if (entity != null) {
libraryCardRepository.save(entity);
}
}
}
}
}
@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttMessageProducer {
//payload或者data是发送消息的内容
//topic是消息发送的主题
//qos是mqtt 对消息处理的几种机制分为0,1,2 其中0表示的是订阅者没收到消息不会再次发送,消息会丢失,1表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息,2相比多了一次去重的动作,确保订阅者收到的消息有一次
//当然,这三种模式下的性能肯定也不一样,qos=0是最好的,2是最差的
//链接到handler,handler跟这些参数怎么约定的?
void sendToMqtt(String data);
void sendToMqtt(String payload, @Header(MqttHeaders.TOPIC) String topic);
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}
2.8 swagger2集成
swagger可以自动生成接口文档,不用再用诸如gitbook生成文档了,也算是解决了一部分事情,引入依赖:
<!-- 整合swagger,用于生成接口 文档 http://localhost:8080/swagger-ui.html-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
初始化配置一下:
//对方法的注解有要求:
//@ApiOperation("获取用户信息")
//@ApiImplicitParam(name = "name", value = "用户名", dataType = "string", paramType = "query")
//@GetMapping("/test/{name}")
//上边的方法是用 @GetMapping 注解,如果只是使用 @RequestMapping 注解,不配置 method 属性,那么 API 文档会生成 7 种请求方式。
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
@Bean
public Docket accessToken() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("api")// 定义组
.select() // 选择那些路径和 api 会生成 document
.apis(RequestHandlerSelectors.basePackage("com.xxx.qingdu.controller")) // 拦截的包路径
.paths(PathSelectors.regex("/*/.*"))// 拦截的接口路径
.build() // 创建
.apiInfo(apiInfo()); // 配置说明
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()//
.title("氢读后台接口")// 标题
.description("相关接口的详细定义")// 描述
.termsOfServiceUrl("http://qingdu.com")//
.contact(new Contact("daniel", "http://qingdu.com", "136099332@qq.com"))// 联系
.version("1.0")// 版本
.build();
}
}
2.9 spring data jpa介绍
关于jpa,已经有一篇文章介绍过了,见这里。其优点也不言而喻:
-
不再为是新增还是更新烦恼;
-
大量的批处理、统计、排序、分页等API;
-
这种方法不但避免了hql语句,可读性也很强:
cartBookRepository.findByIdCartAndIsVisibleOrderByIsChosenDesc(idCart, true)
。
也有一些注意事项:
- save后的对象不是在原参数对象上修改,而是返回是数据库中的数据转换而成的对象,数据库中不含的数据将丢失。
@Override
@Caching(put = {
@CachePut(value = "ggh", key = "'book_idbook_'.concat(#book.id)"),
@CachePut(value = "ggh", key = "'book_isbn_'.concat(#book.publishIsbn)")
})
public Book saveBook(Book book) {
bookRepository.save(book); //注意:返回的是save到book表的内容,不包含其它信息,比如对应数据
return book; //由于数据库的内容与原对象内容没差别,为了保留其它信息,返回原对象
}
2.10 几个小配置
#beautify json
spring.jackson.serialization.indent_output=true
#auto compile
spring.devtools.restart.enabled=true
spring.devtools.restart.exclude=static/**,public/**,templates/**
其它
这两个小依赖背后的功能有点小意思:
<!-- 监控,比如/actuator/httptrace,/actuator/beans -->
<!-- https://www.baeldung.com/spring-boot-actuators -->
<!-- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-endpoints -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 在结果集中新增接口链接提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
2.11 @JsonView的应用
@JsonView是为了在返回数据项时,屏蔽一些字段,刚好在2.4 表单form bean与entity bean分开
介绍了From表单的简化,
这里是对Response的简化。
具体办法是在bean类里的字段上注解,有点分组的意思,一个多版(如果需要的话),一个就是简版了。
先定义两个空接口,实质上是一种约定名称
public interface DetailView extends SimpleView{
}
public interface SimpleView {
}
然后在bean上注解:
@JsonView(DetailView.class)
@Column(name="num_phone")
String numPhone;
@JsonView(SimpleView.class)
@Column(name="id_union")
long idUnion;
最后在controller上引用约定:
@GetMapping("/wechat/{openId}")
@JsonView(SimpleView.class)
public UserByWechat getWechatUserByOpenId(@PathVariable String openId) {
return userWechatService.getWechatUserByOpenId(openId);
}
这样,返回结果时就不会包含num_phone
了,另外没有注解JsonView的字段不会返回:
{
"id": 509028065599152100,
"createTime": "2018-11-05T07:35:48.000+0000",
"lastTime": "2018-11-05T07:37:12.000+0000",
"idUnion": 509028065477517300,
"nameWechat": "string",
"idUnionWechat": "string",
"idOpen": "st"
}
另外,经过实践,其实2.4的做法通过JsonView可以解决,去掉form bean,只有entity bean:
- 返回减元属性以述已经解决
- 防止Formbean的属性把entity bean的覆盖?用
BeanUtils.copyProperties(userMini, exists, new String[] {"id", "idUnion", "createTime", "isDelete"})
, 第三个参数就是解决哪些字段不要拷贝。
@JsonIgnore也有类似功效 https://www.devglan.com/spring-security/spring-boot-jwt-auth
2.12 @JsonProperty的应用
这个是解决返回的json结果的字段名,字段名可以自定义的。特别是在用通用字段时,比如都继承基类字段ID,可读性差,给它重新 起个名字还是有必要的:
@Override
@JsonProperty("idWechartUser")
public long getId() {
return super.getId();
}
这样,返回的结果就变了,而不是上段所示的id
:
{
"createTime" : "2018-11-05T07:35:48.000+0000",
"lastTime" : "2018-11-05T07:37:12.000+0000",
"idUnion" : 509028065477517312,
"nameWechat" : "string",
"idUnionWechat" : "string",
"idOpen" : "st",
"idWechartUser" : 509028065599152128
}
2.13 统一加返回信息的code/error message
简而言之的需求就是:有的是业务逻辑的异常情况,没有throw exception,而是用code/message来封装一个返回数据 ,在之前的项目中是用一个统一的exception handler,业务逻辑的不正常直接throw自定义的exception,然后根据自定义对象 (封装有code/message在java中定义死了/http status)再给Response设定http status以及body。但是,这仅仅处理异常,正常的数据返回走的另一通道。
参考自这里, 解决思路是返回一个map,如果里面包含两项基本内容,code/以及Data(如果没异常)或者errmsg。这样不管是正常或异常的义务处理,都走一条通常。
具体做法如下:
- 定义一个base controller,用途很简单,定义一个return封装,以及一个properties引用
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
//https://github.com/nosqlcoco/springboot-weapp-demo/blob/master/src/main/java/com/weapp/controller/BaseController.java#L21:31
@Component
public abstract class BaseController {
@Autowired
private ImmutableMap<String, String> errorCodeMap;
/**
* 接口数据返回
* @param errorCode
* @param data
* @return
*/
protected Map<String,Object> rtnParam(Integer errorCode, Object data) {
//正常的业务逻辑
if(errorCode == 0){
return ImmutableMap.of("errcode", errorCode, "data", (data == null)? new Object() : data);
}else{
return ImmutableMap.of("errcode", errorCode, "errmsg", errorCodeMap.get(String.valueOf(errorCode)));
}
}
}
ImmutableMap是google的一个工具map,errorCodeMap在程序启动时即初始化:
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import java.io.IOException;
import java.util.Properties;
@EnableCaching //开启缓存功能
@SpringBootApplication //main方式启动
//@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Starter extends org.springframework.boot.web.servlet.support.SpringBootServletInitializer{
private static ImmutableMap<String, String> errorCodeMap = null;
static {
try {
Properties prop = PropertiesLoaderUtils.loadAllProperties("error_code.properties");
errorCodeMap = Maps.fromProperties(prop);
} catch (IOException e) {
e.printStackTrace();
}
}
//覆盖单例,在BaseController引用此对象
@Bean
public ImmutableMap<String, String> errorCodeMap(){
return errorCodeMap;
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Starter.class);
}
public static void main(String[] args) {
SpringApplication.run(Starter.class, args);
}
}
应用也很简单:
//返回一个map
return rtnParam(0, JsonUtil.jsonstr2Map("{\"sessionId\": \"" + sId + "\"}"));
code为0时放data,通常放的是map/bean,会被自动转为json格式。由于controller接口返回的一个map,所以不调rtnParam也是可以的,比如请求第三方接口,其返回是的json,然后我可以直接返回。 当然这时不一定是符合自己定义的数据规范。
2.14 @Cacheable的应用
基本的应用就是在方法上加注解,然后框架自动操作对象与缓存。下面介绍使用redis作为缓存,首先引入基于redis的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置Redis的键生成策略和redis操作template:
import java.lang.reflect.Method;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* redis的缓存配置
* 更多配置可以参考:https://www.cnblogs.com/yueshutong/p/9381540.html
*
* @author tao
*
* TODO 这个类的配置问题
*
*/
@Configuration
@EnableCaching
//https://blog.csdn.net/micro_hz/article/details/76599632
public class RedisConfig extends CachingConfigurerSupport {
//缓存管理器,扩展
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheManager cacheManager = RedisCacheManager.create(factory);
//设置缓存过期时间
//cacheManager.setDefaultExpiration(10000);
return cacheManager;
}
/*//缓存管理器,扩展
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置缓存过期时间
cacheManager.setDefaultExpiration(10000);
return cacheManager;
}*/
@Bean
@Override
public KeyGenerator keyGenerator() {
//未解决的问题:缓存中的内容如何同步更新?
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
//sb.append(target.getClass().getName());
//sb.append(".").append(method.getName()).append("_"); //不能用冒号?
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
//设置序列化工具
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
@SuppressWarnings({ "rawtypes", "unchecked" })
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisTemplate template = new StringRedisTemplate(factory);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
在repository中引用的话,方法如下:
import com.xxx.qingdu.bean.App;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.io.Serializable;
@Repository
@CacheConfig(cacheNames = "app")
public interface AppRepository extends CrudRepository<App, Serializable>, JpaSpecificationExecutor<App> {
//@Cacheable(value="app", keyGenerator="keyGenerator") value指定命名空间,也可以在类上用@CacheConfig(cacheNames = "app")
//坑1:Null key returned for cache operation (maybe you are using named params on classes without debug info?)
//解决办法:不能在repository层定义,或者不设属性key,而是keyGenerator https://www.cnblogs.com/sxdcgaq8080/p/8119081.html
//坑2:java.lang.ClassCastException: com.xxx.qingdu.bean.App cannot be cast to com.xxx.qingdu.bean.App
//解决办法:注释掉热加载的依赖<artifactId>spring-boot-devtools</artifactId>,原因?https://blog.csdn.net/qq_33101675/article/details/78231053
//由于是接口,如何验证cache已生效?在数据表中修改数据,如果读的数据库数据,校验通不过
//更多用法:https://www.cnblogs.com/yueshutong/p/9381540.html https://blog.csdn.net/weixin_41888813/article/details/82754659
@Cacheable(keyGenerator="keyGenerator")
public App findByAppId(String appId);
}
App对象要实现序列化接口,最后,运行时可见redis已经保存了一个appId为键的对象,若要更新这个缓存,在service实现类中使用如下方法:
@CachePut(value="app", key="#app.appId")
public App updateApp(App app) {
return appRespository.save(app);
}
更多注解参考这里
其它实操:
- 取参、拼接key、加条件、以及多项缓存操作定义在一个方法里
@Caching(put = {
@CachePut(value = "ggh", key = "'libraries_idbook_'.concat(#idBook)", condition = "#librariesContainsBook.size() > 0")
},
evict = {
@CacheEvict(value = "ggh", key = "'libraries_idbook_'.concat(#idBook)", condition = "#librariesContainsBook.size() == 0")
}
)
public List<LibraryBook> updateLibraryBookAvailable(long idBook, List<LibraryBook> librariesContainsBook)
- cache注解的方法在同一个类中不能相互调用,否则,并不利用缓存。所以如果数据有一些关联,而存在互调,最好根据不同实体分散在多个service中。